diff --git a/build-macos.bat b/build-macos.bat index 6f65961..b5ca5e0 100644 --- a/build-macos.bat +++ b/build-macos.bat @@ -34,11 +34,11 @@ if errorlevel 1 exit /b 1 go build -o "tools\packmac\packmac.exe" .\tools\packmac if errorlevel 1 exit /b 1 -tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 2.7.0 -arch arm64 +tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 2.7.1 -arch arm64 if errorlevel 1 exit /b 1 -tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 2.7.0 -arch amd64 +tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 2.7.1 -arch amd64 if errorlevel 1 exit /b 1 -tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 2.7.0 -arch universal +tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 2.7.1 -arch universal if errorlevel 1 exit /b 1 echo Built Mac GUI + CLI: diff --git a/dist/navis-release/darwin-arm64/Navis b/dist/navis-release/darwin-arm64/Navis old mode 100644 new mode 100755 index 555d959..57cd02a Binary files a/dist/navis-release/darwin-arm64/Navis and b/dist/navis-release/darwin-arm64/Navis differ diff --git a/dist/navis-release/darwin-arm64/Navis-cli b/dist/navis-release/darwin-arm64/Navis-cli old mode 100644 new mode 100755 index e5a6a05..67ee051 Binary files a/dist/navis-release/darwin-arm64/Navis-cli and b/dist/navis-release/darwin-arm64/Navis-cli differ diff --git a/dist/navis-release/darwin-arm64/Navis.app.zip b/dist/navis-release/darwin-arm64/Navis.app.zip index 2ab4972..5ad25a4 100644 Binary files a/dist/navis-release/darwin-arm64/Navis.app.zip and b/dist/navis-release/darwin-arm64/Navis.app.zip differ diff --git a/dist/navis-release/darwin-arm64/Navis.dmg b/dist/navis-release/darwin-arm64/Navis.dmg index cda76ee..8e0bce2 100644 Binary files a/dist/navis-release/darwin-arm64/Navis.dmg and b/dist/navis-release/darwin-arm64/Navis.dmg differ diff --git a/dist/navis-release/update.json b/dist/navis-release/update.json index fe04ef1..38c187e 100644 --- a/dist/navis-release/update.json +++ b/dist/navis-release/update.json @@ -1,6 +1,6 @@ { - "version": "2.7.0", - "notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli", + "version": "2.7.1", + "notes": "macOS: UDZO DMG, Application Support cores path, naiveproxy asset name fix", "platform": "windows-amd64", "os": "windows", "arch": "amd64", @@ -16,7 +16,7 @@ }, "darwin-arm64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", - "sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d", + "sha256": "8951c351b224d8a13e2772b6ee080d7bee3285ac162dd20c0855a8653a569ec5", "os": "darwin", "arch": "arm64", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg", diff --git a/dist/update.json b/dist/update.json index fe04ef1..38c187e 100644 --- a/dist/update.json +++ b/dist/update.json @@ -1,6 +1,6 @@ { - "version": "2.7.0", - "notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli", + "version": "2.7.1", + "notes": "macOS: UDZO DMG, Application Support cores path, naiveproxy asset name fix", "platform": "windows-amd64", "os": "windows", "arch": "amd64", @@ -16,7 +16,7 @@ }, "darwin-arm64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", - "sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d", + "sha256": "8951c351b224d8a13e2772b6ee080d7bee3285ac162dd20c0855a8653a569ec5", "os": "darwin", "arch": "arm64", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg", diff --git a/internal/config/config.go b/internal/config/config.go index a9e88c8..507fe84 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -207,11 +207,15 @@ func (c *Config) ActiveProfile() (*Profile, error) { } // ResolveBinDir returns an absolute bin directory. -// Relative paths are resolved next to the executable (portable install layout). +// Relative paths are resolved next to the executable (portable install layout), +// or under ~/Library/Application Support/Navis on macOS (.app bundles are read-only). func ResolveBinDir(cfgPath, binDir string) (string, error) { if filepath.IsAbs(binDir) { return binDir, nil } + if dir, ok := platformNavisDir(); ok { + return filepath.Abs(filepath.Join(dir, binDir)) + } if exe, err := os.Executable(); err == nil { return filepath.Abs(filepath.Join(filepath.Dir(exe), binDir)) } @@ -283,9 +287,16 @@ func (c *Config) SetActiveProxy(proxy string) error { return fmt.Errorf("active profile %q not found", c.Active) } -// LocateConfig finds configs/config.json next to the executable or in cwd. +// LocateConfig finds configs/config.json next to the executable, in cwd, +// or under ~/Library/Application Support/Navis on macOS. func LocateConfig() (string, error) { candidates := []string{} + if dir, ok := platformNavisDir(); ok { + candidates = append(candidates, + filepath.Join(dir, "configs", "config.json"), + filepath.Join(dir, "config.json"), + ) + } if exe, err := os.Executable(); err == nil { dir := filepath.Dir(exe) candidates = append(candidates, @@ -301,6 +312,9 @@ func LocateConfig() (string, error) { return p, nil } } + if dir, ok := platformNavisDir(); ok { + return filepath.Join(dir, "configs", "config.json"), nil + } // Default path next to executable for first-run create. if exe, err := os.Executable(); err == nil { return filepath.Join(filepath.Dir(exe), "configs", "config.json"), nil diff --git a/internal/config/paths_darwin.go b/internal/config/paths_darwin.go new file mode 100644 index 0000000..3f633f8 --- /dev/null +++ b/internal/config/paths_darwin.go @@ -0,0 +1,16 @@ +//go:build darwin + +package config + +import ( + "os" + "path/filepath" +) + +func platformNavisDir() (string, bool) { + base, err := os.UserConfigDir() + if err != nil { + return "", false + } + return filepath.Join(base, "Navis"), true +} diff --git a/internal/config/paths_default.go b/internal/config/paths_default.go new file mode 100644 index 0000000..73e5c97 --- /dev/null +++ b/internal/config/paths_default.go @@ -0,0 +1,7 @@ +//go:build !darwin + +package config + +func platformNavisDir() (string, bool) { + return "", false +} diff --git a/internal/protocols/hysteria2/binary.go b/internal/protocols/hysteria2/binary.go index b5dd288..da32488 100644 --- a/internal/protocols/hysteria2/binary.go +++ b/internal/protocols/hysteria2/binary.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -90,6 +91,9 @@ func EnsureBinary(binDir string) (string, error) { return "", err } _ = os.Chmod(dest, 0o755) + if runtime.GOOS == "darwin" { + _ = exec.Command("xattr", "-dr", "com.apple.quarantine", dest).Run() + } return ResolveBinary(binDir) } diff --git a/internal/protocols/naive/binary.go b/internal/protocols/naive/binary.go index c7290ae..f28340b 100644 --- a/internal/protocols/naive/binary.go +++ b/internal/protocols/naive/binary.go @@ -64,7 +64,19 @@ func EnsureBinary(binDir string) (string, error) { if err := extractNaiveArchive(archivePath, binDir); err != nil { return "", err } - return ResolveBinary(binDir) + path, err := ResolveBinary(binDir) + if err != nil { + return "", err + } + clearQuarantine(path) + return path, nil +} + +func clearQuarantine(path string) { + if runtime.GOOS != "darwin" { + return + } + _ = exec.Command("xattr", "-dr", "com.apple.quarantine", path).Run() } func assetNameForPlatform() (string, error) { @@ -76,9 +88,11 @@ func assetNameForPlatform() (string, error) { case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": return "naiveproxy-*-linux-x64.zip", nil case runtime.GOOS == "darwin" && runtime.GOARCH == "amd64": - return "naiveproxy-*-mac-x64.tar.xz", nil + // Newer releases use mac-x64-x64; older used mac-x64. + return "naiveproxy-*-mac-x64", nil case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64": - return "naiveproxy-*-mac-arm64.tar.xz", nil + // Newer releases use mac-arm64-arm64; older used mac-arm64. + return "naiveproxy-*-mac-arm64", nil default: return "", fmt.Errorf("unsupported platform %s/%s for auto-download; place naive binary in bin/", runtime.GOOS, runtime.GOARCH) } @@ -114,14 +128,32 @@ func findReleaseAsset(pattern string) (name, downloadURL string, err error) { return "", "", err } prefix := strings.Split(pattern, "*")[0] - suffix := "" - if parts := strings.Split(pattern, "*"); len(parts) == 2 { - suffix = parts[1] + needle := "" + if parts := strings.SplitN(pattern, "*", 2); len(parts) == 2 { + needle = parts[1] } + var bestName, bestURL string for _, a := range rel.Assets { - if strings.HasPrefix(a.Name, prefix) && strings.HasSuffix(a.Name, suffix) { - return a.Name, a.BrowserDownloadURL, nil + name := a.Name + if strings.Contains(name, "plugin") { + continue } + if !strings.HasPrefix(name, prefix) { + continue + } + if needle != "" && !strings.Contains(name, needle) { + continue + } + // Prefer desktop archives over openwrt/apk. + if strings.HasSuffix(name, ".zip") || strings.HasSuffix(name, ".tar.xz") || strings.HasSuffix(name, ".txz") { + return name, a.BrowserDownloadURL, nil + } + if bestName == "" { + bestName, bestURL = name, a.BrowserDownloadURL + } + } + if bestName != "" { + return bestName, bestURL, nil } return "", "", fmt.Errorf("no asset matching %s in release %s", pattern, rel.TagName) } diff --git a/internal/protocols/xray/binary.go b/internal/protocols/xray/binary.go index 9a87857..1124552 100644 --- a/internal/protocols/xray/binary.go +++ b/internal/protocols/xray/binary.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -78,6 +79,9 @@ func EnsureBinary(binDir string) (string, error) { } } _ = os.Chmod(dest, 0o755) + if runtime.GOOS == "darwin" { + _ = exec.Command("xattr", "-dr", "com.apple.quarantine", dest).Run() + } return ResolveBinary(binDir) } diff --git a/internal/update/update.go b/internal/update/update.go index 924a72d..d278be1 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -17,7 +17,7 @@ import ( ) // CurrentVersion is the shipped client version. -const CurrentVersion = "2.7.0" +const CurrentVersion = "2.7.1" // DefaultManifestURL is the update feed (hosted in the project git repo). const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json" diff --git a/tools/packmac/main.go b/tools/packmac/main.go index 3e93f29..e1a0dd6 100644 --- a/tools/packmac/main.go +++ b/tools/packmac/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "strings" @@ -45,6 +46,9 @@ func run(binPath, outDir, version, arch string) error { if err := writeAppBundle(appRoot, binPath, version, arch); err != nil { return err } + if err := signAppBundle(appRoot); err != nil { + return err + } readme := filepath.Join(stage, "README.txt") if err := os.WriteFile(readme, []byte(readmeText(version, arch)), 0o644); err != nil { return err @@ -67,18 +71,58 @@ func run(binPath, outDir, version, arch string) error { 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 { + if err := writeNativeOrISODmg(dmgPath, stage, "Navis-"+arch); err != nil { return fmt.Errorf("dmg: %w", err) } fmt.Println("wrote", dmgPath) return nil } +// writeNativeOrISODmg prefers hdiutil (real UDIF DMG) on macOS; ISO9660 breaks .app launch. +func writeNativeOrISODmg(dmgPath, stageDir, volume string) error { + if _, err := exec.LookPath("hdiutil"); err == nil { + return writeHdiutilDMG(dmgPath, stageDir, volume) + } + entries := []dmgEntry{} + _ = filepath.Walk(stageDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info == nil { + return err + } + rel, err := filepath.Rel(stageDir, path) + if err != nil || rel == "." { + return err + } + // Only top-level items for ISO packer. + if strings.Contains(rel, string(os.PathSeparator)) { + return nil + } + dst := "/" + filepath.ToSlash(rel) + entries = append(entries, dmgEntry{path, dst}) + return nil + }) + return writeDMG(dmgPath, entries, volume) +} + +func writeHdiutilDMG(dmgPath, stageDir, volume string) error { + _ = os.Remove(dmgPath) + vol := sanitizeVol(volume) + cmd := exec.Command("hdiutil", "create", + "-volname", vol, + "-srcfolder", stageDir, + "-ov", + "-format", "UDZO", + dmgPath, + ) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("hdiutil: %w: %s", err, strings.TrimSpace(string(out))) + } + // Re-sign after packaging is unnecessary for UDZO copy of already-signed .app, + // but clear quarantine on the dmg itself if present. + _ = exec.Command("xattr", "-cr", dmgPath).Run() + return nil +} + func writeAppBundle(appRoot, binPath, version, arch string) error { macOSDir := filepath.Join(appRoot, "Contents", "MacOS") if err := os.MkdirAll(macOSDir, 0o755); err != nil { @@ -92,10 +136,12 @@ func writeAppBundle(appRoot, binPath, version, arch string) error { CFBundleExecutableNavis CFBundleIdentifierwin.evilfox.navis CFBundleNameNavis + CFBundleDisplayNameNavis CFBundlePackageTypeAPPL CFBundleShortVersionString%s CFBundleVersion%s LSMinimumSystemVersion11.0 + NSHighResolutionCapable CFBundleInfoDictionaryVersion6.0%s @@ -103,6 +149,9 @@ func writeAppBundle(appRoot, binPath, version, arch string) error { if err := os.WriteFile(filepath.Join(appRoot, "Contents", "Info.plist"), []byte(plist), 0o644); err != nil { return err } + if err := os.WriteFile(filepath.Join(appRoot, "Contents", "PkgInfo"), []byte("APPL????"), 0o644); err != nil { + return err + } dest := filepath.Join(macOSDir, "Navis") if err := copyFile(binPath, dest, 0o755); err != nil { return err @@ -110,6 +159,25 @@ func writeAppBundle(appRoot, binPath, version, arch string) error { return nil } +func signAppBundle(appRoot string) error { + if _, err := exec.LookPath("codesign"); err != nil { + return nil + } + // Fresh ad-hoc signature; entitlements not required for local/dev builds. + cmd := exec.Command("codesign", "-s", "-", "--force", "--deep", "--options", "runtime", appRoot) + out, err := cmd.CombinedOutput() + if err != nil { + // --options runtime can fail without a real Developer ID; retry plain ad-hoc. + cmd = exec.Command("codesign", "-s", "-", "--force", "--deep", appRoot) + out, err = cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("codesign %s: %w: %s", appRoot, err, strings.TrimSpace(string(out))) + } + } + _ = exec.Command("xattr", "-cr", appRoot).Run() + return nil +} + type dmgEntry struct { srcPath string dstPath string @@ -368,16 +436,17 @@ func sanitizeVol(s string) string { func readmeText(version, arch string) string { switch arch { case "universal": - return fmt.Sprintf(`Navis %s for macOS (универсальная GUI-сборка) + return fmt.Sprintf(`Navis %s for macOS (универсальная сборка) Подходит для Apple Silicon (M1/M2/M3/M4) и Intel Mac. 1) Перетащите Navis.app в Программы (Applications) -2) При блокировке: +2) При первом запуске, если macOS блокирует: xattr -dr com.apple.quarantine /Applications/Navis.app -3) Запустите Navis — откроется окно как на Windows - (Chrome/Edge/Brave в режиме приложения, иначе браузер) -4) CLI (ядро/connect из терминала): рядом на git файл Navis-cli +3) В Терминале: + /Applications/Navis.app/Contents/MacOS/Navis init + /Applications/Navis.app/Contents/MacOS/Navis install-core + /Applications/Navis.app/Contents/MacOS/Navis connect Магазин: https://evilfox.win/ `, version) @@ -467,7 +536,9 @@ func architecturePriorityXML(arch string) string { case "arm64": return ` LSArchitecturePriority - arm64` + arm64 + LSRequiresNativeExecution + ` case "amd64": return ` LSArchitecturePriority diff --git a/versioninfo.json b/versioninfo.json index 2084ac2..844ecaf 100644 --- a/versioninfo.json +++ b/versioninfo.json @@ -11,12 +11,12 @@ "StringFileInfo": { "CompanyName": "EvilFox", "FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)", - "FileVersion": "2.7.0.0", + "FileVersion": "2.7.1.0", "InternalName": "Navis", "LegalCopyright": "Copyright (c) EvilFox", "OriginalFilename": "Navis.exe", "ProductName": "Navis", - "ProductVersion": "2.7.0.0", + "ProductVersion": "2.7.1.0", "Comments": "Open-source VPN/proxy client. https://evilfox.win/" }, "VarFileInfo": {