Release 1.4.1: add macOS DMG and Navis.app.zip to release artifacts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
diskfs "github.com/diskfs/go-diskfs"
|
||||
"github.com/diskfs/go-diskfs/disk"
|
||||
"github.com/diskfs/go-diskfs/filesystem"
|
||||
"github.com/diskfs/go-diskfs/filesystem/iso9660"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bin := flag.String("bin", "", "path to darwin Navis binary")
|
||||
outDir := flag.String("out", "", "output directory (e.g. dist/navis-release/darwin-arm64)")
|
||||
version := flag.String("version", "1.4.1", "CFBundleShortVersionString")
|
||||
arch := flag.String("arch", "arm64", "arch label for volume name")
|
||||
flag.Parse()
|
||||
if *bin == "" || *outDir == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: packmac -bin <Navis> -out <dir> [-version 1.4.1] [-arch arm64]")
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := run(*bin, *outDir, *version, *arch); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "packmac: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(binPath, outDir, version, arch string) error {
|
||||
if err := os.MkdirAll(outDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
stage, err := os.MkdirTemp("", "navis-packmac-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(stage)
|
||||
|
||||
appRoot := filepath.Join(stage, "Navis.app")
|
||||
if err := writeAppBundle(appRoot, binPath, version); err != nil {
|
||||
return err
|
||||
}
|
||||
readme := filepath.Join(stage, "README.txt")
|
||||
if err := os.WriteFile(readme, []byte(readmeText(version, arch)), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zipPath := filepath.Join(outDir, "Navis.app.zip")
|
||||
if err := zipDir(zipPath, stage, []string{"Navis.app", "README.txt"}); err != nil {
|
||||
return fmt.Errorf("zip: %w", err)
|
||||
}
|
||||
fmt.Println("wrote", zipPath)
|
||||
|
||||
dmgPath := filepath.Join(outDir, "Navis.dmg")
|
||||
if err := writeDMG(dmgPath, appRoot, readme, "Navis-"+arch); err != nil {
|
||||
return fmt.Errorf("dmg: %w", err)
|
||||
}
|
||||
fmt.Println("wrote", dmgPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAppBundle(appRoot, binPath, version string) error {
|
||||
macOSDir := filepath.Join(appRoot, "Contents", "MacOS")
|
||||
if err := os.MkdirAll(macOSDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
plist := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key><string>Navis</string>
|
||||
<key>CFBundleIdentifier</key><string>win.evilfox.navis</string>
|
||||
<key>CFBundleName</key><string>Navis</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>%s</string>
|
||||
<key>CFBundleVersion</key><string>%s</string>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
`, version, version)
|
||||
if err := os.WriteFile(filepath.Join(appRoot, "Contents", "Info.plist"), []byte(plist), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
dest := filepath.Join(macOSDir, "Navis")
|
||||
if err := copyFile(binPath, dest, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeDMG(dmgPath, appRoot, readmePath, volume string) error {
|
||||
_ = os.Remove(dmgPath)
|
||||
appSize, err := dirSize(appRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
readmeSize, err := fileSize(readmePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
need := appSize + readmeSize + 4*1024*1024
|
||||
if need < 12*1024*1024 {
|
||||
need = 12 * 1024 * 1024
|
||||
}
|
||||
// round up to 2048
|
||||
need = ((need + 2047) / 2048) * 2048
|
||||
|
||||
mydisk, err := diskfs.Create(dmgPath, need, diskfs.SectorSizeDefault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mydisk.LogicalBlocksize = 2048
|
||||
fs, err := mydisk.CreateFilesystem(disk.FilesystemSpec{
|
||||
Partition: 0,
|
||||
FSType: filesystem.TypeISO9660,
|
||||
VolumeLabel: sanitizeVol(volume),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := copyTreeToISO(fs, appRoot, "/Navis.app"); err != nil {
|
||||
_ = fs.Close()
|
||||
return err
|
||||
}
|
||||
if err := writeISOFile(fs, "/README.txt", readmePath, 0o644); err != nil {
|
||||
_ = fs.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
iso, ok := fs.(*iso9660.FileSystem)
|
||||
if !ok {
|
||||
_ = fs.Close()
|
||||
return fmt.Errorf("not iso9660")
|
||||
}
|
||||
if err := iso.Finalize(iso9660.FinalizeOptions{
|
||||
RockRidge: true,
|
||||
Joliet: true,
|
||||
DeepDirectories: true,
|
||||
VolumeIdentifier: sanitizeVol(volume),
|
||||
PublisherIdentifier: "Navis",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.Close()
|
||||
}
|
||||
|
||||
func copyTreeToISO(fs filesystem.FileSystem, srcRoot, dstRoot string) error {
|
||||
return filepath.Walk(srcRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(srcRoot, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
dst := dstRoot
|
||||
if rel != "." {
|
||||
dst = dstRoot + "/" + rel
|
||||
}
|
||||
if info.IsDir() {
|
||||
if dst == dstRoot {
|
||||
return fs.Mkdir(dst)
|
||||
}
|
||||
return fs.Mkdir(dst)
|
||||
}
|
||||
mode := info.Mode()
|
||||
if mode&0o111 != 0 || strings.HasSuffix(rel, "MacOS/Navis") || strings.HasSuffix(rel, "MacOS\\Navis") {
|
||||
mode = 0o755
|
||||
} else {
|
||||
mode = 0o644
|
||||
}
|
||||
return writeISOFile(fs, dst, path, mode)
|
||||
})
|
||||
}
|
||||
|
||||
func writeISOFile(fs filesystem.FileSystem, dst, src string, mode os.FileMode) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
rw, err := fs.OpenFile(dst, os.O_CREATE|os.O_RDWR|os.O_TRUNC)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(rw, in); err != nil {
|
||||
_ = rw.Close()
|
||||
return err
|
||||
}
|
||||
if err := rw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fs.Chmod(dst, mode); err != nil {
|
||||
_ = err // best-effort
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func zipDir(zipPath, stage string, entries []string) error {
|
||||
_ = os.Remove(zipPath)
|
||||
f, err := os.Create(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
zw := zip.NewWriter(f)
|
||||
defer zw.Close()
|
||||
|
||||
for _, name := range entries {
|
||||
root := filepath.Join(stage, name)
|
||||
info, err := os.Stat(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
if err := addZipFile(zw, root, name, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(stage, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
if fi.IsDir() {
|
||||
_, err := zw.Create(rel + "/")
|
||||
return err
|
||||
}
|
||||
mode := os.FileMode(0o644)
|
||||
if strings.Contains(rel, "Contents/MacOS/") {
|
||||
mode = 0o755
|
||||
}
|
||||
return addZipFile(zw, path, rel, mode)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addZipFile(zw *zip.Writer, path, name string, mode os.FileMode) error {
|
||||
h, err := zip.FileInfoHeader(mustStat(path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Name = name
|
||||
h.Method = zip.Deflate
|
||||
h.SetMode(mode)
|
||||
w, err := zw.CreateHeader(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
_, err = io.Copy(w, in)
|
||||
return err
|
||||
}
|
||||
|
||||
func mustStat(path string) os.FileInfo {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fi
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, mode os.FileMode) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, in)
|
||||
return err
|
||||
}
|
||||
|
||||
func dirSize(root string) (int64, error) {
|
||||
var n int64
|
||||
err := filepath.Walk(root, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
n += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return n, err
|
||||
}
|
||||
|
||||
func fileSize(path string) (int64, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return fi.Size(), nil
|
||||
}
|
||||
|
||||
func sanitizeVol(s string) string {
|
||||
s = strings.ToUpper(s)
|
||||
s = strings.Map(func(r rune) rune {
|
||||
if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
||||
return r
|
||||
}
|
||||
return '-'
|
||||
}, s)
|
||||
if len(s) > 32 {
|
||||
s = s[:32]
|
||||
}
|
||||
if s == "" {
|
||||
s = "NAVIS"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func readmeText(version, arch string) string {
|
||||
return fmt.Sprintf(`Navis %s for macOS (%s)
|
||||
|
||||
1) Open this disk image
|
||||
2) Drag Navis.app to Applications (or any folder)
|
||||
3) First launch may need:
|
||||
xattr -dr com.apple.quarantine /Applications/Navis.app
|
||||
4) Run from Terminal:
|
||||
/Applications/Navis.app/Contents/MacOS/Navis init
|
||||
/Applications/Navis.app/Contents/MacOS/Navis install-core
|
||||
/Applications/Navis.app/Contents/MacOS/Navis connect
|
||||
|
||||
Or use the CLI binary next to this DMG on git (file named Navis).
|
||||
|
||||
Shop: https://evilfox.win/
|
||||
`, version, arch)
|
||||
}
|
||||
Reference in New Issue
Block a user