Release 3.8.0.2: app.zip updates, lock hygiene, secret-safe getState.

This commit is contained in:
M4
2026-07-29 22:00:39 +03:00
parent 54b5b87990
commit 6a7480dceb
31 changed files with 2058 additions and 1537 deletions
+128 -33
View File
@@ -22,7 +22,7 @@ const CurrentVersion = "3.8.0"
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
const BuildNumber = 1
const BuildNumber = 2
// 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"
@@ -82,21 +82,44 @@ func PlatformKey() string {
return runtime.GOOS + "-" + runtime.GOARCH
}
// AssetFor returns download URL + checksum for the given platform key.
// Asset kinds returned by ResolveAsset.
const (
AssetKindBinary = "bin"
AssetKindAppZip = "appzip"
)
// AssetFor returns Mach-O / exe URL + checksum for the platform key.
func (m Manifest) AssetFor(key string) (url, sha string, ok bool) {
url, sha, _, ok = m.ResolveAsset(key, false)
return url, sha, ok
}
// ResolveAsset picks binary or .app.zip. PreferAppZip when running inside Navis.app.
func (m Manifest) ResolveAsset(key string, preferAppZip bool) (url, sha, kind string, ok bool) {
if key == "" {
key = PlatformKey()
}
if m.Platforms != nil {
if a, found := m.Platforms[key]; found && strings.TrimSpace(a.URL) != "" {
return strings.TrimSpace(a.URL), strings.TrimSpace(a.SHA256), true
if a, found := m.Platforms[key]; found {
if preferAppZip {
zu := strings.TrimSpace(a.ZipURL)
zs := strings.TrimSpace(a.ZipSHA256)
if zu != "" && zs != "" {
return zu, zs, AssetKindAppZip, true
}
}
u := strings.TrimSpace(a.URL)
s := strings.TrimSpace(a.SHA256)
if u != "" {
return u, s, AssetKindBinary, true
}
}
}
// Legacy single-asset manifests are windows-amd64.
if (key == "windows-amd64" || key == "windows-386") && strings.TrimSpace(m.URL) != "" {
return strings.TrimSpace(m.URL), strings.TrimSpace(m.SHA256), true
return strings.TrimSpace(m.URL), strings.TrimSpace(m.SHA256), AssetKindBinary, true
}
return "", "", false
return "", "", "", false
}
// Check fetches the manifest and compares versions for this platform.
@@ -116,7 +139,15 @@ func Check(ctx context.Context, manifestURL string) (Status, error) {
st.Notes = m.Notes
st.Mandatory = m.Mandatory
url, sha, ok := m.AssetFor(key)
preferZip := false
if runtime.GOOS == "darwin" {
if exe, err := os.Executable(); err == nil {
if _, ok := AppBundleRoot(exe); ok {
preferZip = true
}
}
}
url, sha, _, ok := m.ResolveAsset(key, preferZip)
if !ok {
st.Error = fmt.Sprintf("нет сборки для %s", key)
st.Available = false
@@ -247,60 +278,124 @@ func fileSHA256(path string) (string, error) {
return hex.EncodeToString(h.Sum(nil)), nil
}
// preparedUpdate is a verified download ready for platform Apply.
type preparedUpdate struct {
Latest string
URL string
SHA256 string
ExePath string
TmpPath string
Kind string // AssetKindBinary | AssetKindAppZip
AppRoot string // set when Kind is appzip
}
// AppBundleRoot returns the .app path when exe lives in Contents/MacOS.
func AppBundleRoot(exe string) (string, bool) {
exe = filepath.Clean(exe)
const marker = ".app" + string(filepath.Separator) + "Contents" + string(filepath.Separator) + "MacOS"
idx := strings.Index(exe, marker)
if idx < 0 {
// Also accept forward-slash form after EvalSymlinks on some volumes.
idx = strings.Index(exe, ".app/Contents/MacOS")
if idx < 0 {
return "", false
}
return exe[:idx+len(".app")], true
}
return exe[:idx+len(".app")], true
}
// prepareDownload fetches the update next to the running binary as temp download.
// Refuses when already on/newer than the feed product version (no re-apply loop).
func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha, exePath, tmpPath string, err error) {
st, err := Check(ctx, manifestURL)
p, err := prepareUpdate(ctx, manifestURL)
if err != nil {
return "", "", "", "", "", err
}
return p.Latest, p.URL, p.SHA256, p.ExePath, p.TmpPath, nil
}
func prepareUpdate(ctx context.Context, manifestURL string) (preparedUpdate, error) {
st, err := Check(ctx, manifestURL)
if err != nil {
return preparedUpdate{}, err
}
if !st.Available {
if st.Error != "" {
return "", "", "", "", "", fmt.Errorf("%s", st.Error)
return preparedUpdate{}, fmt.Errorf("%s", st.Error)
}
return "", "", "", "", "", fmt.Errorf("уже актуальная версия %s (канал %s)", DisplayVersion(), st.Latest)
}
if !allowedDownloadURL(st.URL) {
return "", "", "", "", "", fmt.Errorf("URL обновления должен быть на git.evilfox.cc или files.de4ima.uk")
return preparedUpdate{}, fmt.Errorf("уже актуальная версия %s (канал %s)", DisplayVersion(), st.Latest)
}
exe, err := os.Executable()
if err != nil {
return "", "", "", "", "", err
return preparedUpdate{}, err
}
exe, err = filepath.Abs(exe)
if err != nil {
return "", "", "", "", "", err
return preparedUpdate{}, err
}
if resolved, err := filepath.EvalSymlinks(exe); err == nil && resolved != "" {
exe = resolved
}
tmp := filepath.Join(filepath.Dir(exe), ".navis-update-download")
_ = os.Remove(tmp)
// Also clear legacy leftover from older updaters.
_ = os.Remove(exe + ".new")
if err := downloadFile(ctx, st.URL, tmp); err != nil {
return "", "", "", "", "", err
}
wantSHA := st.SHA256
if wantSHA == "" {
if man, err := fetchManifest(ctx, manifestURL); err == nil {
if _, sha2, ok := man.AssetFor(PlatformKey()); ok {
wantSHA = sha2
}
preferZip := false
appRoot := ""
if runtime.GOOS == "darwin" {
if root, ok := AppBundleRoot(exe); ok {
preferZip = true
appRoot = root
}
}
man, err := fetchManifest(ctx, manifestURL)
if err != nil {
return preparedUpdate{}, err
}
url, wantSHA, kind, ok := man.ResolveAsset(PlatformKey(), preferZip)
if !ok || url == "" {
return preparedUpdate{}, fmt.Errorf("нет ассета обновления для %s", PlatformKey())
}
if !allowedDownloadURL(url) {
return preparedUpdate{}, fmt.Errorf("URL обновления должен быть на git.evilfox.cc или files.de4ima.uk")
}
if wantSHA == "" {
_ = os.Remove(tmp)
return "", "", "", "", "", fmt.Errorf("в манифесте нет sha256 — обновление отклонено")
return preparedUpdate{}, fmt.Errorf("в манифесте нет sha256 — обновление отклонено")
}
// Prefer .app.zip when available; otherwise replace Mach-O inside the bundle (legacy).
if kind != AssetKindAppZip {
appRoot = ""
}
dir := filepath.Dir(exe)
if appRoot != "" {
dir = filepath.Dir(appRoot) // sibling of .app
}
tmpName := ".navis-update-download"
if kind == AssetKindAppZip {
tmpName = ".navis-update-app.zip"
}
tmp := filepath.Join(dir, tmpName)
_ = os.Remove(tmp)
_ = os.Remove(exe + ".new")
if err := downloadFile(ctx, url, tmp); err != nil {
return preparedUpdate{}, err
}
sum, err := fileSHA256(tmp)
if err != nil {
_ = os.Remove(tmp)
return "", "", "", "", "", err
return preparedUpdate{}, err
}
if !strings.EqualFold(sum, wantSHA) {
_ = os.Remove(tmp)
return "", "", "", "", "", fmt.Errorf("checksum mismatch")
return preparedUpdate{}, fmt.Errorf("checksum mismatch")
}
return st.Latest, st.URL, wantSHA, exe, tmp, nil
return preparedUpdate{
Latest: st.Latest,
URL: url,
SHA256: wantSHA,
ExePath: exe,
TmpPath: tmp,
Kind: kind,
AppRoot: appRoot,
}, nil
}