Files
navi/tools/packmac/main.go
T

479 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, arch); err != nil {
return err
}
readme := filepath.Join(stage, "README.txt")
if err := os.WriteFile(readme, []byte(readmeText(version, arch)), 0o644); err != nil {
return err
}
warnName := archWarningFilename(arch)
if warnName != "" {
if err := os.WriteFile(filepath.Join(stage, warnName), []byte(archWarningText(arch)), 0o644); err != nil {
return err
}
}
zipEntries := []string{"Navis.app", "README.txt"}
if warnName != "" {
zipEntries = append(zipEntries, warnName)
}
zipPath := filepath.Join(outDir, "Navis.app.zip")
if err := zipDir(zipPath, stage, zipEntries); err != nil {
return fmt.Errorf("zip: %w", err)
}
fmt.Println("wrote", zipPath)
dmgPath := filepath.Join(outDir, "Navis.dmg")
dmgFiles := []dmgEntry{{appRoot, "/Navis.app"}}
if warnName != "" {
dmgFiles = append(dmgFiles, dmgEntry{filepath.Join(stage, warnName), "/" + warnName})
}
dmgFiles = append(dmgFiles, dmgEntry{readme, "/README.txt"})
if err := writeDMG(dmgPath, dmgFiles, "Navis-"+arch); err != nil {
return fmt.Errorf("dmg: %w", err)
}
fmt.Println("wrote", dmgPath)
return nil
}
func writeAppBundle(appRoot, binPath, version, arch string) error {
macOSDir := filepath.Join(appRoot, "Contents", "MacOS")
if err := os.MkdirAll(macOSDir, 0o755); err != nil {
return err
}
archPriority := architecturePriorityXML(arch)
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>%s
</dict>
</plist>
`, version, version, archPriority)
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
}
type dmgEntry struct {
srcPath string
dstPath string
}
func writeDMG(dmgPath string, files []dmgEntry, volume string) error {
_ = os.Remove(dmgPath)
var need int64
for _, f := range files {
if fi, err := os.Stat(f.srcPath); err == nil && !fi.IsDir() {
need += fi.Size()
} else if sz, err := dirSize(f.srcPath); err == nil {
need += sz
}
}
need += 4 * 1024 * 1024
if need < 12*1024*1024 {
need = 12 * 1024 * 1024
}
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
}
for _, f := range files {
info, err := os.Stat(f.srcPath)
if err != nil {
_ = fs.Close()
return err
}
if info.IsDir() {
if err := copyTreeToISO(fs, f.srcPath, f.dstPath); err != nil {
_ = fs.Close()
return err
}
continue
}
if err := writeISOFile(fs, f.dstPath, f.srcPath, 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 {
switch arch {
case "universal":
return fmt.Sprintf(`Navis %s for macOS (универсальная GUI-сборка)
Подходит для Apple Silicon (M1/M2/M3/M4) и Intel Mac.
1) Перетащите Navis.app в Программы (Applications)
2) При блокировке:
xattr -dr com.apple.quarantine /Applications/Navis.app
3) Запустите Navis — откроется окно как на Windows
(Chrome/Edge/Brave в режиме приложения, иначе браузер)
4) CLI (ядро/connect из терминала): рядом на git файл Navis-cli
Магазин: https://evilfox.win/
`, version)
case "arm64":
return fmt.Sprintf(`Navis %s for macOS (Apple Silicon: M1/M2/M3/M4)
⚠️ Только для Mac с чипом Apple Silicon (arm64).
На Intel Mac скачайте darwin-amd64 или darwin-universal.
1) Перетащите Navis.app в Программы
2) xattr -dr com.apple.quarantine /Applications/Navis.app
3) /Applications/Navis.app/Contents/MacOS/Navis connect
Магазин: https://evilfox.win/
`, version)
case "amd64":
return fmt.Sprintf(`Navis %s for macOS (Intel x86_64)
⚠️ Только для Intel Mac.
На Mac M1/M2/M3/M4 эта сборка НЕ запустится — скачайте darwin-arm64 или darwin-universal.
1) Перетащите Navis.app в Программы
2) xattr -dr com.apple.quarantine /Applications/Navis.app
3) /Applications/Navis.app/Contents/MacOS/Navis connect
Магазин: https://evilfox.win/
`, version)
default:
return fmt.Sprintf(`Navis %s for macOS (%s)
1) Drag Navis.app to Applications
2) xattr -dr com.apple.quarantine /Applications/Navis.app
3) /Applications/Navis.app/Contents/MacOS/Navis connect
`, version, arch)
}
}
func archWarningFilename(arch string) string {
switch arch {
case "amd64":
return "ТОЛЬКО-INTEL-MAC.txt"
case "arm64":
return "ТОЛЬКО-APPLE-SILICON.txt"
default:
return ""
}
}
func archWarningText(arch string) string {
switch arch {
case "amd64":
return `ВНИМАНИЕ
Эта сборка Navis только для Intel Mac (x86_64).
Если у вас Mac mini / MacBook с чипом M1, M2, M3 или M4 —
закройте этот диск и скачайте универсальный DMG:
https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg
Или arm64:
https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg
`
case "arm64":
return `ВНИМАНИЕ
Эта сборка Navis только для Apple Silicon (M1/M2/M3/M4).
Если у вас Intel Mac — скачайте darwin-amd64 или универсальный DMG:
https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg
`
default:
return ""
}
}
func architecturePriorityXML(arch string) string {
switch arch {
case "universal":
return `
<key>LSArchitecturePriority</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>`
case "arm64":
return `
<key>LSArchitecturePriority</key>
<array><string>arm64</string></array>`
case "amd64":
return `
<key>LSArchitecturePriority</key>
<array><string>x86_64</string></array>`
default:
return ""
}
}