Merge origin/main: Navis 2.7.2+2 with app icon and update UX.

Combine remote update check/skip flow with green N AppIcon packaging for macOS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
M4
2026-07-29 18:08:01 +03:00
co-authored by Cursor
35 changed files with 255 additions and 264 deletions
+7
View File
@@ -243,6 +243,13 @@ https://evilfox.win/
Некоторые AV (Bkav, Microsoft Wacapew/Wacatac, Ikarus Trojan.WinGo.Agent, Google, Trapmine) часто помечают **любые** неподписанные Go-бинарники с сетью и сменой системного прокси.
В 2.7.2:
- убран вечный цикл обновления (проверка больше не устанавливает сама; только кнопка «Обновить»);
- номер сборки в версии (`2.7.2+N` / FileVersion `2.7.2.N`); сравнение обновлений только по product semver.
В 2.7.1:
- обновление только по кнопке «Обновить»; можно «Пропустить эту версию» (следующую всё равно предложит).
В 2.7.0:
- GUI на macOS (тот же интерфейс, что на Windows): DMG → Navis.app открывает окно клиента; CLI лежит как `Navis-cli`.
+3 -2
View File
@@ -12,8 +12,9 @@ android {
applicationId = "win.evilfox.navis"
minSdk = 26
targetSdk = 35
versionCode = 270
versionName = "2.7.0"
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
versionCode = 2_070_201
versionName = "2.7.2+1"
vectorDrawables.useSupportLibrary = true
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
+9 -26
View File
@@ -1,21 +1,7 @@
@echo off
setlocal EnableDelayedExpansion
setlocal
cd /d "%~dp0"
REM --- bump build number ---
for /f "usebackq delims=" %%i in (`python -c "from pathlib import Path; import re; t=Path('internal/update/version.go').read_text(); print(re.search(r'BaseVersion\\s*=\\s*\"([^\"]+)\"', t).group(1))"`) do set BASE_VERSION=%%i
if not exist buildnum.txt echo 0>buildnum.txt
set /p BUILD_NUM=<buildnum.txt
set /a BUILD_NUM+=1
echo %BUILD_NUM%>buildnum.txt
set FULL_VERSION=%BASE_VERSION%.%BUILD_NUM%
echo Building macOS Navis %FULL_VERSION% ...
python -c "from pathlib import Path; import re; p=Path('internal/update/version.go'); t=p.read_text(); p.write_text(re.sub(r'(var BuildNumber = \")[^\"]*(\")', r'\g<1>%BUILD_NUM%\2', t))"
python -c "import json; from pathlib import Path; maj,mi,pa=map(int,'%BASE_VERSION%'.split('.')[:3]); b=int('%BUILD_NUM%'); p=Path('versioninfo.json'); d=json.loads(p.read_text()); d['FixedFileInfo']['FileVersion']={'Major':maj,'Minor':mi,'Patch':pa,'Build':b}; d['FixedFileInfo']['ProductVersion']=d['FixedFileInfo']['FileVersion']; d['StringFileInfo']['FileVersion']=f'{maj}.{mi}.{pa}.{b}'; d['StringFileInfo']['ProductVersion']=d['StringFileInfo']['FileVersion']; p.write_text(json.dumps(d,indent=2)+chr(10))"
set LDFLAGS=-s -w -X vpnclient/internal/update.BuildNumber=%BUILD_NUM%
mkdir "dist\navis-release\darwin-arm64" 2>nul
mkdir "dist\navis-release\darwin-amd64" 2>nul
mkdir "dist\navis-release\darwin-universal" 2>nul
@@ -23,18 +9,16 @@ mkdir "dist\navis-release\darwin-universal" 2>nul
set CGO_ENABLED=0
set GOOS=darwin
set GOARCH=arm64
go build -ldflags="%LDFLAGS%" -trimpath -o "dist\navis-release\darwin-arm64\Navis" .\cmd\vpnapp
go build -ldflags="-s -w" -o "dist\navis-release\darwin-arm64\Navis" .\cmd\vpnapp
if errorlevel 1 exit /b 1
go build -ldflags="%LDFLAGS%" -trimpath -o "dist\navis-release\darwin-arm64\Navis-cli" .\cmd\vpnclient
go build -ldflags="-s -w" -o "dist\navis-release\darwin-arm64\Navis-cli" .\cmd\vpnclient
if errorlevel 1 exit /b 1
echo %FULL_VERSION%> "dist\navis-release\darwin-arm64\VERSION"
set GOARCH=amd64
go build -ldflags="%LDFLAGS%" -trimpath -o "dist\navis-release\darwin-amd64\Navis" .\cmd\vpnapp
go build -ldflags="-s -w" -o "dist\navis-release\darwin-amd64\Navis" .\cmd\vpnapp
if errorlevel 1 exit /b 1
go build -ldflags="%LDFLAGS%" -trimpath -o "dist\navis-release\darwin-amd64\Navis-cli" .\cmd\vpnclient
go build -ldflags="-s -w" -o "dist\navis-release\darwin-amd64\Navis-cli" .\cmd\vpnclient
if errorlevel 1 exit /b 1
echo %FULL_VERSION%> "dist\navis-release\darwin-amd64\VERSION"
set GOOS=
set GOARCH=
@@ -46,19 +30,18 @@ tools\mklipo\mklipo.exe "dist\navis-release\darwin-amd64\Navis" "dist\navis-rele
if errorlevel 1 exit /b 1
tools\mklipo\mklipo.exe "dist\navis-release\darwin-amd64\Navis-cli" "dist\navis-release\darwin-arm64\Navis-cli" "dist\navis-release\darwin-universal\Navis-cli"
if errorlevel 1 exit /b 1
echo %FULL_VERSION%> "dist\navis-release\darwin-universal\VERSION"
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 %FULL_VERSION% -build %BUILD_NUM% -arch arm64
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 2.7.2 -build 2.7.2.2 -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 %FULL_VERSION% -build %BUILD_NUM% -arch amd64
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 2.7.2 -build 2.7.2.2 -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 %FULL_VERSION% -build %BUILD_NUM% -arch universal
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 2.7.2 -build 2.7.2.2 -arch universal
if errorlevel 1 exit /b 1
echo Built Mac GUI + CLI %FULL_VERSION%:
echo Built Mac GUI + CLI:
dir "dist\navis-release\darwin-universal"
dir "dist\navis-release\darwin-arm64"
dir "dist\navis-release\darwin-amd64"
+4 -21
View File
@@ -1,19 +1,7 @@
@echo off
setlocal EnableDelayedExpansion
setlocal
cd /d "%~dp0"
REM --- bump build number ---
for /f "usebackq delims=" %%i in (`python -c "from pathlib import Path; import re; t=Path('internal/update/version.go').read_text(); print(re.search(r'BaseVersion\\s*=\\s*\"([^\"]+)\"', t).group(1))"`) do set BASE_VERSION=%%i
if not exist buildnum.txt echo 0>buildnum.txt
set /p BUILD_NUM=<buildnum.txt
set /a BUILD_NUM+=1
echo %BUILD_NUM%>buildnum.txt
set FULL_VERSION=%BASE_VERSION%.%BUILD_NUM%
echo Building Windows Navis %FULL_VERSION% ...
python -c "from pathlib import Path; import re; p=Path('internal/update/version.go'); t=p.read_text(); p.write_text(re.sub(r'(var BuildNumber = \")[^\"]*(\")', r'\g<1>%BUILD_NUM%\2', t))"
python -c "import json; from pathlib import Path; maj,mi,pa=map(int,'%BASE_VERSION%'.split('.')[:3]); b=int('%BUILD_NUM%'); p=Path('versioninfo.json'); d=json.loads(p.read_text()); d['FixedFileInfo']['FileVersion']={'Major':maj,'Minor':mi,'Patch':pa,'Build':b}; d['FixedFileInfo']['ProductVersion']=d['FixedFileInfo']['FileVersion']; d['StringFileInfo']['FileVersion']=f'{maj}.{mi}.{pa}.{b}'; d['StringFileInfo']['ProductVersion']=d['StringFileInfo']['FileVersion']; p.write_text(json.dumps(d,indent=2)+chr(10))"
echo Generating Windows icon resources...
go run ./tools/mkico
if errorlevel 1 exit /b 1
@@ -26,21 +14,16 @@ if errorlevel 1 (
goversioninfo -64 -icon assets\navis.ico -manifest assets\app.manifest -o cmd\vpnapp\resource.syso versioninfo.json
if errorlevel 1 exit /b 1
set LDFLAGS=-H windowsgui -s -w -X vpnclient/internal/update.BuildNumber=%BUILD_NUM%
echo Building Navis GUI and CLI...
go build -ldflags="%LDFLAGS%" -trimpath -o Navis.exe ./cmd/vpnapp
go build -ldflags="-H windowsgui -s -w" -trimpath -o Navis.exe ./cmd/vpnapp
if errorlevel 1 exit /b 1
go build -ldflags="-s -w -X vpnclient/internal/update.BuildNumber=%BUILD_NUM%" -trimpath -o vpnclient.exe ./cmd/vpnclient
go build -ldflags="-s -w" -trimpath -o vpnclient.exe ./cmd/vpnclient
if errorlevel 1 exit /b 1
echo %FULL_VERSION%> Navis.version
echo.
echo Done: Navis %FULL_VERSION%
echo Done:
echo Navis.exe
echo vpnclient.exe
echo Navis.version
echo assets\navis.ico
endlocal
-1
View File
@@ -1 +0,0 @@
4
+16 -11
View File
@@ -1,4 +1,4 @@
//go:build windows
//go:build windows
package main
@@ -92,7 +92,7 @@ func main() {
cfgPath: cfgPath,
logBuf: logBuf,
updateStatus: update.Status{
Current: update.CurrentVersion,
Current: update.DisplayVersion(),
ManifestURL: update.DefaultManifestURL,
},
}
@@ -204,7 +204,7 @@ func (a *app) getState() (uiState, error) {
CorePath: corePath,
ConfigPath: a.cfgPath,
Profiles: cfg.ListProfiles(),
Version: update.CurrentVersion,
Version: update.DisplayVersion(),
Update: a.updateStatus,
Pings: append([]netcheck.Result(nil), a.pings...),
Subscription: cfg.SubscriptionURL,
@@ -418,7 +418,7 @@ func (a *app) checkUpdate() (update.Status, error) {
st, err := update.Check(ctx, update.DefaultManifestURL)
a.mu.Lock()
if err != nil {
st.Current = update.CurrentVersion
st.Current = update.DisplayVersion()
st.ManifestURL = update.DefaultManifestURL
st.Error = err.Error()
}
@@ -428,6 +428,14 @@ func (a *app) checkUpdate() (update.Status, error) {
}
func (a *app) applyUpdate() (string, error) {
// Explicit user action only — never called from autoCheckUpdate.
st, err := a.checkUpdate()
if err != nil {
return "", err
}
if !st.Available {
return "", fmt.Errorf("обновление не требуется (текущая %s)", update.DisplayVersion())
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
latest, err := update.Apply(ctx, update.DefaultManifestURL)
@@ -444,14 +452,11 @@ func (a *app) applyUpdate() (string, error) {
}
func (a *app) autoCheckUpdate() {
// Only refresh update status for the UI banner — never auto-install.
// (Previously auto-apply caused an infinite update loop when the feed version
// stayed ahead of the shipped CurrentVersion in the downloaded binary.)
time.Sleep(3 * time.Second)
st, err := a.checkUpdate()
if err != nil || !st.Available {
return
}
// Auto-apply + relaunch when a newer build is published.
time.Sleep(1500 * time.Millisecond)
_, _ = a.applyUpdate()
_, _ = a.checkUpdate()
}
func openURL(raw string) error {
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
2.7.2.4
2.7.2+2
+1 -1
View File
@@ -1 +1 @@
2.7.2.4
2.7.2.2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7 -7
View File
@@ -1,22 +1,22 @@
{
"version": "2.7.2.4",
"notes": "Navis 2.7.2.4",
"version": "2.7.2",
"notes": "Navis 2.7.2+2: иконка приложения (N), macOS AppIcon; обновление только по кнопке.",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "4f1f34f57ed7cd588f9d62f24d074041be3f3b1504f18e8203c953536236c381",
"sha256": "0a6d7297e132f9835e2b82f5f28ec1d4e2a51fc070343f8c701e019ac57a2e86",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
@@ -24,7 +24,7 @@
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
"sha256": "8072cacd79fa95da3f3230b88b8e09e380ed2e9f634703f579950952b94f774d",
"os": "darwin",
"arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
@@ -32,7 +32,7 @@
},
"darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
"sha256": "f50642373f615fa55117d1bbe7b2f86f2a19ab03a9ae53cc7e7d137b432ebbe5",
"os": "darwin",
"arch": "universal",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
Binary file not shown.
+7 -7
View File
@@ -1,22 +1,22 @@
{
"version": "2.7.2.4",
"notes": "Navis 2.7.2.4",
"version": "2.7.2",
"notes": "Navis 2.7.2+2: иконка приложения (N), macOS AppIcon; обновление только по кнопке.",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "4f1f34f57ed7cd588f9d62f24d074041be3f3b1504f18e8203c953536236c381",
"sha256": "0a6d7297e132f9835e2b82f5f28ec1d4e2a51fc070343f8c701e019ac57a2e86",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
@@ -24,7 +24,7 @@
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
"sha256": "8072cacd79fa95da3f3230b88b8e09e380ed2e9f634703f579950952b94f774d",
"os": "darwin",
"arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
@@ -32,7 +32,7 @@
},
"darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
"sha256": "f50642373f615fa55117d1bbe7b2f86f2a19ab03a9ae53cc7e7d137b432ebbe5",
"os": "darwin",
"arch": "universal",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
+14 -9
View File
@@ -70,7 +70,7 @@ func New(mgr *core.Manager, cfgPath string, logBuf *bytes.Buffer) *App {
CfgPath: cfgPath,
LogBuf: logBuf,
UpdateStatus: update.Status{
Current: update.CurrentVersion,
Current: update.DisplayVersion(),
ManifestURL: update.DefaultManifestURL,
},
}
@@ -125,7 +125,7 @@ func (a *App) GetState() (UIState, error) {
CorePath: corePath,
ConfigPath: a.CfgPath,
Profiles: cfg.ListProfiles(),
Version: update.CurrentVersion,
Version: update.DisplayVersion(),
Update: a.UpdateStatus,
Pings: append([]netcheck.Result(nil), a.Pings...),
Subscription: cfg.SubscriptionURL,
@@ -322,7 +322,7 @@ func (a *App) CheckUpdate() (update.Status, error) {
st, err := update.Check(ctx, update.DefaultManifestURL)
a.mu.Lock()
if err != nil {
st.Current = update.CurrentVersion
st.Current = update.DisplayVersion()
st.ManifestURL = update.DefaultManifestURL
st.Error = err.Error()
}
@@ -332,6 +332,14 @@ func (a *App) CheckUpdate() (update.Status, error) {
}
func (a *App) ApplyUpdate() (string, error) {
// Explicit user action only — never called from AutoCheckUpdate.
st, err := a.CheckUpdate()
if err != nil {
return "", err
}
if !st.Available {
return "", fmt.Errorf("обновление не требуется (текущая %s)", update.DisplayVersion())
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
latest, err := update.Apply(ctx, update.DefaultManifestURL)
@@ -351,13 +359,10 @@ func (a *App) ApplyUpdate() (string, error) {
}
func (a *App) AutoCheckUpdate() {
// Only check and populate status for the banner — never auto-apply.
// Auto-apply previously caused endless download→relaunch loops on Mac/Windows.
time.Sleep(3 * time.Second)
st, err := a.CheckUpdate()
if err != nil || !st.Available {
return
}
time.Sleep(1500 * time.Millisecond)
_, _ = a.ApplyUpdate()
_, _ = a.CheckUpdate()
}
func (a *App) OpenShopURL(raw string) error {
+41 -4
View File
@@ -76,6 +76,15 @@
color: var(--muted);
font-size: .84rem;
}
.update-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.update-actions .action {
flex: 1 1 auto;
min-width: 140px;
}
.top {
display: flex;
@@ -712,7 +721,10 @@
<div class="update-banner" id="updateBanner">
<strong id="updateTitle">Доступно обновление</strong>
<p id="updateNotes"></p>
<button class="action primary" id="updateBtn" type="button">Обновить и перезапустить</button>
<div class="update-actions">
<button class="action primary" id="updateBtn" type="button">Обновить</button>
<button class="action secondary" id="skipUpdateBtn" type="button">Пропустить эту версию</button>
</div>
</div>
<header class="top">
@@ -941,7 +953,18 @@
const updCheckBtn = $("updCheckBtn");
const updateBanner = $("updateBanner");
const updateBtn = $("updateBtn");
const skipUpdateBtn = $("skipUpdateBtn");
const verLabel = $("verLabel");
const SKIP_UPDATE_KEY = "navis.skipUpdateVersion";
function skippedVersion() {
try { return localStorage.getItem(SKIP_UPDATE_KEY) || ""; } catch (_) { return ""; }
}
function setSkippedVersion(v) {
try {
if (v) localStorage.setItem(SKIP_UPDATE_KEY, String(v));
else localStorage.removeItem(SKIP_UPDATE_KEY);
} catch (_) {}
}
const quitBtn = $("quitBtn");
if (window.__navisHttp && quitBtn) {
quitBtn.hidden = false;
@@ -1178,10 +1201,17 @@
updateBanner.classList.remove("show");
return;
}
const latest = (u.latest || "").toString();
const skip = skippedVersion();
if (u.available && latest && skip === latest) {
updateBanner.classList.remove("show");
return;
}
if (u.available) {
updateBanner.classList.add("show");
$("updateTitle").textContent = "Обновление " + u.latest;
$("updateNotes").textContent = u.notes || ("Текущая версия " + u.current);
$("updateTitle").textContent = "Доступно обновление " + latest;
$("updateNotes").textContent = u.notes || ("У вас " + (u.current || version || "?") + ". Обновление только по кнопке.");
updateBanner.dataset.latest = latest;
} else {
updateBanner.classList.remove("show");
}
@@ -1284,7 +1314,7 @@
}
function paintButtonsLocked(locked) {
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, updateBtn, subBtn, subUrl].forEach((b) => {
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, updateBtn, skipUpdateBtn, subBtn, subUrl].forEach((b) => {
if (b) b.disabled = locked;
});
}
@@ -1449,6 +1479,13 @@
} catch (e) { setMeta(String(e), "err"); }
}));
skipUpdateBtn.addEventListener("click", () => {
const latest = updateBanner.dataset.latest || "";
if (latest) setSkippedVersion(latest);
updateBanner.classList.remove("show");
setMeta("Версия " + (latest || "?") + " пропущена. Следующую предложим.", "ok");
});
(async () => {
try {
await refresh({ syncForm: true });
+52 -25
View File
@@ -16,9 +16,27 @@ import (
"time"
)
// CurrentVersion is the product/semver used for update eligibility (feed "version").
// Keep major.minor.patch only — no build suffix here.
const CurrentVersion = "2.7.2"
// 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 = 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"
// DisplayVersion is shown in the UI (product + build), e.g. "2.7.2+1".
func DisplayVersion() string {
return fmt.Sprintf("%s+%d", CurrentVersion, BuildNumber)
}
// FileVersionDots is Major.Minor.Patch.Build for Windows resources / CFBundleVersion.
func FileVersionDots() string {
return fmt.Sprintf("%s.%d", CurrentVersion, BuildNumber)
}
// PlatformAsset is one OS/arch binary in the feed.
type PlatformAsset struct {
URL string `json:"url"`
@@ -87,7 +105,7 @@ func Check(ctx context.Context, manifestURL string) (Status, error) {
manifestURL = DefaultManifestURL
}
key := PlatformKey()
st := Status{Current: CurrentVersion, ManifestURL: manifestURL, Platform: key}
st := Status{Current: DisplayVersion(), ManifestURL: manifestURL, Platform: key}
m, err := fetchManifest(ctx, manifestURL)
if err != nil {
@@ -106,38 +124,46 @@ func Check(ctx context.Context, manifestURL string) (Status, error) {
}
st.URL = url
st.SHA256 = sha
// Product-only compare: build numbers must not keep offering an update after install
// when feed version equals CurrentVersion (e.g. 2.7.2 vs local 2.7.2+1).
st.Available = Compare(st.Latest, CurrentVersion) > 0 && url != ""
return st, nil
}
// Compare returns 1 if a>b, -1 if a<b, 0 if equal (semver-ish dotted ints).
// Compare returns 1 if a>b, -1 if a<b, 0 if equal.
//
// Update rule: only product semver (major.minor.patch) counts. Build metadata
// ("+N", "-suffix", or a 4th dotted component) is ignored so a just-updated app
// on the same product version is never treated as outdated vs the same feed.
func Compare(a, b string) int {
as := splitVer(a)
bs := splitVer(b)
n := len(as)
if len(bs) > n {
n = len(bs)
}
for i := 0; i < n; i++ {
var ai, bi int
if i < len(as) {
ai = as[i]
}
if i < len(bs) {
bi = bs[i]
}
if ai > bi {
as := productParts(a)
bs := productParts(b)
for i := 0; i < 3; i++ {
if as[i] > bs[i] {
return 1
}
if ai < bi {
if as[i] < bs[i] {
return -1
}
}
return 0
}
func productParts(v string) [3]int {
parts := splitVer(v)
var out [3]int
for i := 0; i < 3 && i < len(parts); i++ {
out[i] = parts[i]
}
return out
}
func splitVer(v string) []int {
v = strings.TrimPrefix(strings.TrimSpace(v), "v")
// Strip build metadata: 2.7.2+1 / 2.7.2-beta → compare product only.
if i := strings.IndexAny(v, "+-"); i >= 0 {
v = v[:i]
}
parts := strings.Split(v, ".")
out := make([]int, 0, len(parts))
for _, p := range parts {
@@ -146,9 +172,6 @@ func splitVer(v string) []int {
out = append(out, 0)
continue
}
if i := strings.IndexAny(p, "-+"); i >= 0 {
p = p[:i]
}
n, _ := strconv.Atoi(p)
out = append(out, n)
}
@@ -169,7 +192,7 @@ func fetchManifest(ctx context.Context, manifestURL string) (Manifest, error) {
if err != nil {
return Manifest{}, err
}
req.Header.Set("User-Agent", "Navis/"+CurrentVersion)
req.Header.Set("User-Agent", "Navis/"+DisplayVersion())
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req)
@@ -192,7 +215,7 @@ func downloadFile(ctx context.Context, url, dest string) error {
if err != nil {
return err
}
req.Header.Set("User-Agent", "Navis/"+CurrentVersion)
req.Header.Set("User-Agent", "Navis/"+DisplayVersion())
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Do(req)
if err != nil {
@@ -224,7 +247,8 @@ func fileSHA256(path string) (string, error) {
return hex.EncodeToString(h.Sum(nil)), nil
}
// prepareDownload fetches the update next to the running binary as "<exe>.new".
// 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)
if err != nil {
@@ -234,7 +258,7 @@ func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha,
if st.Error != "" {
return "", "", "", "", "", fmt.Errorf("%s", st.Error)
}
return "", "", "", "", "", fmt.Errorf("обновление не найдено")
return "", "", "", "", "", fmt.Errorf("уже актуальная версия %s (канал %s)", DisplayVersion(), st.Latest)
}
if !allowedDownloadURL(st.URL) {
return "", "", "", "", "", fmt.Errorf("URL обновления должен быть на git.evilfox.cc или files.de4ima.uk")
@@ -247,6 +271,9 @@ func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha,
if err != nil {
return "", "", "", "", "", 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.
+13
View File
@@ -15,6 +15,19 @@ func TestCompare(t *testing.T) {
if Compare("1.1.0", "1.1.0") != 0 {
t.Fatal("expected equal")
}
// Build must not make same product look outdated or newer for update checks.
if Compare("2.7.2", "2.7.2+9") != 0 {
t.Fatal("expected equal ignoring +build")
}
if Compare("2.7.2", "2.7.2.99") != 0 {
t.Fatal("expected equal ignoring 4th component")
}
if Compare("2.7.3", "2.7.2+1") <= 0 {
t.Fatal("expected product bump to win")
}
if DisplayVersion() == "" || !strings.Contains(DisplayVersion(), "+") {
t.Fatal("DisplayVersion should include build")
}
}
func TestManifestAssetFor(t *testing.T) {
-31
View File
@@ -1,31 +0,0 @@
package update
import "strings"
// BaseVersion is the marketing / release version (MAJOR.MINOR.PATCH).
const BaseVersion = "2.7.2"
// BuildNumber is the incremental build counter.
// Override at link time:
//
// -ldflags "-X vpnclient/internal/update.BuildNumber=42"
//
// Source default is bumped by build scripts via buildnum.txt.
var BuildNumber = "4"
// CurrentVersion is BaseVersion.BuildNumber, e.g. "2.7.2.1".
// Set in init so -ldflags BuildNumber is applied first.
var CurrentVersion string
func init() {
CurrentVersion = FullVersion()
}
// FullVersion returns "MAJOR.MINOR.PATCH.BUILD".
func FullVersion() string {
b := strings.TrimSpace(BuildNumber)
if b == "" {
b = "0"
}
return BaseVersion + "." + b
}
+27 -55
View File
@@ -1,67 +1,39 @@
#!/bin/bash
# Build Navis GUI for Apple Silicon (arm64) with auto-incremented build number.
# Build Navis GUI for Apple Silicon (arm64).
set -euo pipefail
cd "$(dirname "$0")/.."
BASE_VERSION="$(python3 - <<'PY'
VERSION="$(python3 - <<'PY'
import re
from pathlib import Path
t = Path('internal/update/version.go').read_text()
m = re.search(r'BaseVersion\s*=\s*"([^"]+)"', t)
t = Path('internal/update/update.go').read_text()
m = re.search(r'CurrentVersion\s*=\s*"([^"]+)"', t)
print(m.group(1) if m else '0.0.0')
PY
)"
BUILD_NUM="$(tr -d '[:space:]' < buildnum.txt 2>/dev/null || echo 0)"
if ! [[ "$BUILD_NUM" =~ ^[0-9]+$ ]]; then BUILD_NUM=0; fi
BUILD_NUM=$((BUILD_NUM + 1))
echo "$BUILD_NUM" > buildnum.txt
FULL_VERSION="${BASE_VERSION}.${BUILD_NUM}"
echo "Building Navis ${FULL_VERSION} (arm64)..."
# Keep source BuildNumber in sync for non-ldflags runs / IDE
python3 - <<PY
from pathlib import Path
p = Path('internal/update/version.go')
t = p.read_text()
BUILD="$(python3 - <<'PY'
import re
t2 = re.sub(r'(var BuildNumber = ")[^"]*(")', r'\g<1>${BUILD_NUM}\2', t)
p.write_text(t2)
PY
# versioninfo.json (Windows metadata; also documents build)
python3 - <<PY
import json, re
from pathlib import Path
maj, minor, patch = [int(x) for x in "${BASE_VERSION}".split(".")[:3]]
build = int("${BUILD_NUM}")
full = "${FULL_VERSION}"
p = Path('versioninfo.json')
d = json.loads(p.read_text())
d['FixedFileInfo']['FileVersion'] = {"Major": maj, "Minor": minor, "Patch": patch, "Build": build}
d['FixedFileInfo']['ProductVersion'] = {"Major": maj, "Minor": minor, "Patch": patch, "Build": build}
d['StringFileInfo']['FileVersion'] = f"{full}.0" if full.count('.')==3 else full
d['StringFileInfo']['ProductVersion'] = d['StringFileInfo']['FileVersion']
# Prefer 4-part a.b.c.d
d['StringFileInfo']['FileVersion'] = f"{maj}.{minor}.{patch}.{build}"
d['StringFileInfo']['ProductVersion'] = f"{maj}.{minor}.{patch}.{build}"
p.write_text(json.dumps(d, indent=2) + "\n")
print('versioninfo.json ->', d['StringFileInfo']['FileVersion'])
t = Path('internal/update/update.go').read_text()
m = re.search(r'const BuildNumber\s*=\s*(\d+)', t)
print(m.group(1) if m else '0')
PY
)"
FULL="${VERSION}.${BUILD}"
echo "Building Navis ${VERSION}+${BUILD} (arm64)..."
OUT="dist/navis-release/darwin-arm64"
mkdir -p "$OUT"
LDFLAGS="-s -w -X vpnclient/internal/update.BuildNumber=${BUILD_NUM}"
LDFLAGS="-s -w"
export PATH="/tmp/go-sdk/go/bin:/usr/local/go/bin:$PATH"
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \
go build -ldflags="$LDFLAGS" -trimpath -o "$OUT/Navis" ./cmd/vpnapp
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \
go build -ldflags="$LDFLAGS" -trimpath -o "$OUT/Navis-cli" ./cmd/vpnclient
# Stamp plain-text version next to binaries
printf '%s\n' "$FULL_VERSION" > "$OUT/VERSION"
printf '%s\n' "$FULL_VERSION" > "$OUT/Navis.version"
printf '%s\n' "$FULL" > "$OUT/VERSION"
printf '%s\n' "${VERSION}+${BUILD}" > "$OUT/Navis.version"
if command -v codesign >/dev/null 2>&1; then
codesign -s - --force "$OUT/Navis"
@@ -72,35 +44,35 @@ go build -o tools/packmac/packmac ./tools/packmac
tools/packmac/packmac \
-bin "$OUT/Navis" \
-out "$OUT" \
-version "$FULL_VERSION" \
-build "$BUILD_NUM" \
-version "$VERSION" \
-build "$FULL" \
-arch arm64
# Rename release copies with version in filename (keep unversioned too)
cp -f "$OUT/Navis.dmg" "$OUT/Navis-${FULL_VERSION}-arm64.dmg"
cp -f "$OUT/Navis.app.zip" "$OUT/Navis-${FULL_VERSION}-arm64.app.zip"
cp -f "$OUT/Navis.dmg" "$OUT/Navis-${FULL}-arm64.dmg"
cp -f "$OUT/Navis.app.zip" "$OUT/Navis-${FULL}-arm64.app.zip"
# update.json
python3 - <<PY
import hashlib, json
from pathlib import Path
full = "${FULL_VERSION}"
ver = "${VERSION}"
build = "${BUILD}"
full = "${FULL}"
binp = Path('dist/navis-release/darwin-arm64/Navis')
h = hashlib.sha256(binp.read_bytes()).hexdigest()
notes = f"Navis {ver}+{build}: иконка приложения (N), macOS AppIcon; обновление только по кнопке."
for p in [Path('dist/update.json'), Path('dist/navis-release/update.json')]:
if not p.exists():
continue
d = json.loads(p.read_text())
d['version'] = full
d['notes'] = f'Navis {full}'
d['version'] = ver
d['notes'] = notes
plats = d.setdefault('platforms', {})
if 'darwin-arm64' in plats:
plats['darwin-arm64']['sha256'] = h
p.write_text(json.dumps(d, ensure_ascii=False, indent=2) + '\n')
print('updated', p, '->', full)
print('updated', p, '->', ver, 'sha', h[:12])
PY
echo ""
echo "Done: Navis ${FULL_VERSION}"
echo "Done: Navis ${VERSION}+${BUILD}"
ls -lh "$OUT"
file "$OUT/Navis.dmg"
+1 -1
View File
@@ -17,7 +17,7 @@ DEST="${1:-$HOME/Downloads/Navis.dmg}"
UNIVERSAL="$BASE/darwin-universal/Navis.dmg"
SPECIFIC="$BASE/$SUBDIR/Navis.dmg"
echo "Mac: $ARCH · ставим Navis GUI 2.7"
echo "Mac: $ARCH · ставим Navis GUI 2.7.1"
echo "Скачиваю универсальный DMG…"
if ! curl -fL -o "$DEST" "$UNIVERSAL"; then
echo "Universal недоступен, скачиваю $SUBDIR"
+7 -7
View File
@@ -1,22 +1,22 @@
{
"version": "2.7.0",
"notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
"version": "2.7.2",
"notes": "2.7.2: исправлен вечный цикл обновления (только кнопка «Обновить», без автоустановки); номер сборки 2.7.2+N; можно «Пропустить эту версию».",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
"sha256": "8fa7fcb40d4165d20d3f81840c84f729af034b54dd4698ea144c7145c85872eb",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
@@ -24,7 +24,7 @@
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
"sha256": "8072cacd79fa95da3f3230b88b8e09e380ed2e9f634703f579950952b94f774d",
"os": "darwin",
"arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
@@ -32,7 +32,7 @@
},
"darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
"sha256": "f50642373f615fa55117d1bbe7b2f86f2a19ab03a9ae53cc7e7d137b432ebbe5",
"os": "darwin",
"arch": "universal",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
+39 -49
View File
@@ -19,33 +19,25 @@ import (
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 (e.g. 2.7.2.3)")
build := flag.String("build", "", "CFBundleVersion build number (default: last segment of -version)")
version := flag.String("version", "1.4.1", "CFBundleShortVersionString")
build := flag.String("build", "", "CFBundleVersion (defaults to -version)")
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 2.7.2.3] [-build 3] [-arch arm64]")
fmt.Fprintln(os.Stderr, "usage: packmac -bin <Navis> -out <dir> [-version 2.7.2] [-build 2.7.2.1] [-arch arm64]")
os.Exit(2)
}
buildNo := *build
if buildNo == "" {
buildNo = lastVersionSegment(*version)
bundleVer := *build
if bundleVer == "" {
bundleVer = *version
}
if err := run(*bin, *outDir, *version, buildNo, *arch); err != nil {
if err := run(*bin, *outDir, *version, bundleVer, *arch); err != nil {
fmt.Fprintf(os.Stderr, "packmac: %v\n", err)
os.Exit(1)
}
}
func lastVersionSegment(v string) string {
v = strings.TrimSpace(v)
if i := strings.LastIndex(v, "."); i >= 0 && i+1 < len(v) {
return v[i+1:]
}
return v
}
func run(binPath, outDir, version, buildNo, arch string) error {
func run(binPath, outDir, version, bundleVersion, arch string) error {
if err := os.MkdirAll(outDir, 0o755); err != nil {
return err
}
@@ -56,7 +48,7 @@ func run(binPath, outDir, version, buildNo, arch string) error {
defer os.RemoveAll(stage)
appRoot := filepath.Join(stage, "Navis.app")
if err := writeAppBundle(appRoot, binPath, version, buildNo, arch); err != nil {
if err := writeAppBundle(appRoot, binPath, version, bundleVersion, arch); err != nil {
return err
}
if err := signAppBundle(appRoot); err != nil {
@@ -136,7 +128,7 @@ func writeHdiutilDMG(dmgPath, stageDir, volume string) error {
return nil
}
func writeAppBundle(appRoot, binPath, version, buildNo, arch string) error {
func writeAppBundle(appRoot, binPath, version, bundleVersion, arch string) error {
macOSDir := filepath.Join(appRoot, "Contents", "MacOS")
resDir := filepath.Join(appRoot, "Contents", "Resources")
if err := os.MkdirAll(macOSDir, 0o755); err != nil {
@@ -163,7 +155,7 @@ func writeAppBundle(appRoot, binPath, version, buildNo, arch string) error {
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>%s
</dict>
</plist>
`, version, buildNo, archPriority)
`, version, bundleVersion, archPriority)
if err := os.WriteFile(filepath.Join(appRoot, "Contents", "Info.plist"), []byte(plist), 0o644); err != nil {
return err
}
@@ -179,39 +171,9 @@ func writeAppBundle(appRoot, binPath, version, buildNo, arch string) error {
if err := copyFile(binPath, dest, 0o755); err != nil {
return err
}
// Embed human-readable version next to the binary inside the .app
_ = os.WriteFile(filepath.Join(macOSDir, "VERSION"), []byte(version+"\n"), 0o644)
return nil
}
// findAsset locates a file under assets/ relative to cwd or the packmac binary.
func findAsset(name string) string {
candidates := []string{
filepath.Join("assets", name),
}
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
candidates = append(candidates,
filepath.Join(dir, "..", "..", "assets", name),
filepath.Join(dir, "..", "assets", name),
filepath.Join(dir, "assets", name),
)
}
if wd, err := os.Getwd(); err == nil {
candidates = append(candidates, filepath.Join(wd, "assets", name))
}
for _, c := range candidates {
if st, err := os.Stat(c); err == nil && !st.IsDir() {
abs, err := filepath.Abs(c)
if err == nil {
return abs
}
return c
}
}
return ""
}
func signAppBundle(appRoot string) error {
if _, err := exec.LookPath("codesign"); err != nil {
return nil
@@ -432,6 +394,34 @@ func mustStat(path string) os.FileInfo {
return fi
}
// findAsset locates a file under assets/ relative to cwd or the packmac binary.
func findAsset(name string) string {
candidates := []string{
filepath.Join("assets", name),
}
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
candidates = append(candidates,
filepath.Join(dir, "..", "..", "assets", name),
filepath.Join(dir, "..", "assets", name),
filepath.Join(dir, "assets", name),
)
}
if wd, err := os.Getwd(); err == nil {
candidates = append(candidates, filepath.Join(wd, "assets", name))
}
for _, c := range candidates {
if st, err := os.Stat(c); err == nil && !st.IsDir() {
abs, err := filepath.Abs(c)
if err == nil {
return abs
}
return c
}
}
return ""
}
func copyFile(src, dst string, mode os.FileMode) error {
in, err := os.Open(src)
if err != nil {
+5 -5
View File
@@ -4,13 +4,13 @@
"Major": 2,
"Minor": 7,
"Patch": 2,
"Build": 4
"Build": 2
},
"ProductVersion": {
"Major": 2,
"Minor": 7,
"Patch": 2,
"Build": 4
"Build": 2
},
"FileFlagsMask": "3f",
"FileFlags": "00",
@@ -20,13 +20,13 @@
},
"StringFileInfo": {
"CompanyName": "EvilFox",
"FileDescription": "Navis 2 \u2014 VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
"FileVersion": "2.7.2.4",
"FileDescription": "Navis 2 VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
"FileVersion": "2.7.2.2",
"InternalName": "Navis",
"LegalCopyright": "Copyright (c) EvilFox",
"OriginalFilename": "Navis.exe",
"ProductName": "Navis",
"ProductVersion": "2.7.2.4",
"ProductVersion": "2.7.2.2",
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
},
"VarFileInfo": {