Release 3.8.2.4: unify Windows/macOS GUI on glaze HTTP host.
ci / test (macos-latest) (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run

Auto-bump build number on each compile; ship versioned Windows artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
M4
2026-08-01 16:01:25 +03:00
co-authored by Cursor
parent d1570b23d3
commit 464a61aebf
26 changed files with 406 additions and 256 deletions
+6 -2
View File
@@ -6,8 +6,8 @@
| OS | Артефакты | UI |
|----|----------|----|
| Windows amd64 | `Navis.exe` | GUI (WebView2) |
| macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) |
| Windows amd64 | `Navis.exe` | GUI (glaze → WebView2 + HTTP UI) |
| macOS universal | `Navis.dmg` · `Navis.app.zip` | GUI (glaze → WKWebView + HTTP UI) |
| macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI |
| Android | `Navis.apk` | GUI (VpnService) |
@@ -192,10 +192,14 @@ cd ~/Navis
## Сборка Windows
GUI — тот же HTML UI, что и на macOS: локальный HTTP API + окно [glaze](https://github.com/crgimenes/glaze) (на Windows под капотом Edge WebView2 Runtime).
```bat
build.bat
```
Артефакты: `Navis-<version>.<build>.exe`, `vpnclient-<version>.<build>.exe` (и копии в `dist\navis-release\`).
или:
```bat
+2 -2
View File
@@ -13,8 +13,8 @@ android {
minSdk = 26
targetSdk = 35
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
versionCode = 3080203
versionName = "3.8.2+3"
versionCode = 3080204
versionName = "3.8.2+4"
vectorDrawables.useSupportLibrary = true
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 18 KiB

+12 -4
View File
@@ -1,7 +1,15 @@
@echo off
setlocal
setlocal EnableExtensions
cd /d "%~dp0"
echo Bumping build number...
where py >nul 2>&1 && py -3 scripts\bump-build.py && goto :bumped
where python >nul 2>&1 && python scripts\bump-build.py && goto :bumped
where python3 >nul 2>&1 && python3 scripts\bump-build.py && goto :bumped
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\bump-build.ps1"
if errorlevel 1 exit /b 1
:bumped
mkdir "dist\navis-release\darwin-arm64" 2>nul
mkdir "dist\navis-release\darwin-amd64" 2>nul
mkdir "dist\navis-release\darwin-universal" 2>nul
@@ -34,11 +42,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 3.8.2 -build 3.8.2.3 -arch arm64
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.2 -build 3.8.2.4 -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 3.8.2 -build 3.8.2.3 -arch amd64
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.2 -build 3.8.2.4 -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 3.8.2 -build 3.8.2.3 -arch universal
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.2 -build 3.8.2.4 -arch universal
if errorlevel 1 exit /b 1
echo Built Mac GUI + CLI:
+66 -6
View File
@@ -1,7 +1,24 @@
@echo off
setlocal
setlocal EnableExtensions
cd /d "%~dp0"
echo Bumping build number...
call :bump_build
if errorlevel 1 exit /b 1
REM Full version = CurrentVersion.BuildNumber from versioninfo.json (e.g. 3.8.2.4)
set "VER="
for /f "usebackq delims=" %%v in (`powershell -NoProfile -Command "(Get-Content -Raw 'versioninfo.json' | ConvertFrom-Json).StringFileInfo.FileVersion"`) do set "VER=%%v"
if not defined VER (
echo Failed to read FileVersion from versioninfo.json
exit /b 1
)
set "OUT_GUI=Navis-%VER%.exe"
set "OUT_CLI=vpnclient-%VER%.exe"
set "DIST=dist\navis-release"
echo Building Navis %VER% for Windows...
echo Generating Windows icon resources...
go run ./tools/mkico
if errorlevel 1 exit /b 1
@@ -14,16 +31,59 @@ 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
echo Building Navis GUI and CLI...
go build -ldflags="-H windowsgui -s -w" -trimpath -o Navis.exe ./cmd/vpnapp
echo Building GUI -^> %OUT_GUI%
go build -ldflags="-H windowsgui -s -w" -trimpath -o "%OUT_GUI%" ./cmd/vpnapp
if errorlevel 1 exit /b 1
go build -ldflags="-s -w" -trimpath -o vpnclient.exe ./cmd/vpnclient
echo Building CLI -^> %OUT_CLI%
go build -ldflags="-s -w" -trimpath -o "%OUT_CLI%" ./cmd/vpnclient
if errorlevel 1 exit /b 1
mkdir "%DIST%" 2>nul
copy /Y "%OUT_GUI%" "%DIST%\%OUT_GUI%" >nul
if errorlevel 1 exit /b 1
copy /Y "%OUT_CLI%" "%DIST%\%OUT_CLI%" >nul
if errorlevel 1 exit /b 1
REM Stable names for the update feed / installers (best-effort if file is locked)
call :copy_alias "%OUT_GUI%" "%DIST%\Navis.exe"
call :copy_alias "%OUT_CLI%" "%DIST%\vpnclient.exe"
echo.
echo Done:
echo Navis.exe
echo vpnclient.exe
echo %OUT_GUI%
echo %OUT_CLI%
echo %DIST%\%OUT_GUI%
echo %DIST%\%OUT_CLI%
echo %DIST%\Navis.exe (update alias)
echo assets\navis.ico
endlocal
exit /b 0
:bump_build
where py >nul 2>&1
if not errorlevel 1 (
py -3 scripts\bump-build.py
exit /b %ERRORLEVEL%
)
where python >nul 2>&1
if not errorlevel 1 (
python scripts\bump-build.py
exit /b %ERRORLEVEL%
)
where python3 >nul 2>&1
if not errorlevel 1 (
python3 scripts\bump-build.py
exit /b %ERRORLEVEL%
)
REM PowerShell fallback when Python is not installed
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\bump-build.ps1"
exit /b %ERRORLEVEL%
:copy_alias
if exist "%~2" del /F /Q "%~2" >nul 2>&1
copy /Y "%~1" "%~2" >nul 2>&1
if errorlevel 1 (
echo Warning: could not refresh %~2 ^(file in use^). Versioned build is still ready.
exit /b 0
)
exit /b 0
@@ -1,16 +1,13 @@
//go:build darwin
//go:build windows || darwin
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"runtime"
"syscall"
"time"
"github.com/crgimenes/glaze"
@@ -28,6 +25,7 @@ func main() {
return
}
update.CleanupStaleDownloads()
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU())
@@ -44,6 +42,7 @@ func main() {
if err != nil {
fatalf("Ошибка конфига %s: %v", cfgPath, err)
}
logBuf := logbuf.New(0)
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
if err != nil {
@@ -51,10 +50,9 @@ func main() {
}
a := apphost.New(mgr, cfgPath, logBuf)
a.OpenURL = func(u string) error {
return exec.Command("open", u).Start()
}
a.OpenURL = openExternal
a.OnAfterUpdate = func() {
// Hard-exit so the OS unlocks the binary for replacement.
go func() {
time.Sleep(300 * time.Millisecond)
_ = a.Disconnect()
@@ -71,6 +69,7 @@ func main() {
go func() {
_ = srv.Serve(ln)
}()
a.StartBackground()
trayhost.Start("Navis", trayhost.Hooks{
Connect: func() { _ = a.Connect() },
@@ -86,12 +85,13 @@ func main() {
w, err := glaze.New(false)
if err != nil || w == nil {
log.Printf("webview unavailable (%v); opening default browser", err)
if err2 := exec.Command("open", uiURL).Run(); err2 != nil {
if err2 := openExternal(uiURL); err2 != nil {
_ = srv.Close()
fatalf("Не удалось открыть интерфейс:\n%v\n%v\n%s", err, err2, uiURL)
}
log.Printf("Navis UI: %s", uiURL)
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
signal.Notify(sig, shutdownSignals()...)
<-sig
_ = mgr.Disconnect()
_ = srv.Close()
@@ -105,14 +105,8 @@ func main() {
w.SetTitle("Navis")
w.SetSize(500, 900, glaze.HintNone)
decorateWindow(w)
w.Navigate(uiURL)
log.Printf("Navis UI: %s", uiURL)
w.Run()
}
func fatalf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
log.Println(msg)
_ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "Navis" message %q`, msg)).Run()
os.Exit(1)
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
)
func main() {
fmt.Fprintln(os.Stderr, "Navis GUI supports Windows (WebView2) and macOS.")
fmt.Fprintln(os.Stderr, "Navis GUI supports Windows and macOS (glaze + local HTTP UI).")
fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./Navis connect | install-core | check-update")
os.Exit(1)
}
-202
View File
@@ -1,202 +0,0 @@
//go:build windows
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"time"
"unsafe"
"github.com/jchv/go-webview2"
"golang.org/x/sys/windows"
"vpnclient/internal/apphost"
"vpnclient/internal/appui"
"vpnclient/internal/config"
"vpnclient/internal/core"
"vpnclient/internal/logbuf"
"vpnclient/internal/trayhost"
"vpnclient/internal/update"
)
func main() {
if update.MaybeFinishUpdate(os.Args[1:]) {
return
}
update.CleanupStaleDownloads()
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU())
cfgPath, err := config.LocateConfig()
if err != nil {
fatalDialog("Не удалось найти путь конфига: %v", err)
}
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
if err := config.WriteExample(cfgPath); err != nil {
fatalDialog("Не удалось создать конфиг: %v", err)
}
}
cfg, err := config.Load(cfgPath)
if err != nil {
fatalDialog("Ошибка конфига %s: %v", cfgPath, err)
}
logBuf := logbuf.New(0)
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
if err != nil {
fatalDialog("%v", err)
}
a := apphost.New(mgr, cfgPath, logBuf)
a.OpenURL = shellOpen
a.OnAfterUpdate = func() {
// Hard-exit so Windows unlocks Navis.exe; do not wait on webview.Terminate.
go func() {
time.Sleep(300 * time.Millisecond)
_ = a.Disconnect()
os.Exit(0)
}()
}
dataPath := filepath.Join(filepath.Dir(cfgPath), ".navis-webview")
_ = os.MkdirAll(dataPath, 0o755)
w := webview2.NewWithOptions(webview2.WebViewOptions{
Debug: false,
AutoFocus: true,
DataPath: dataPath,
WindowOptions: webview2.WindowOptions{
Title: "Navis",
Width: 500,
Height: 900,
Center: true,
IconId: 1,
},
})
if w == nil {
fatalDialog("Не удалось создать окно WebView2.\nУстановите Microsoft Edge WebView2 Runtime.")
}
defer func() {
_ = mgr.Disconnect()
w.Destroy()
}()
applyWindowIcon(w.Window())
w.SetSize(500, 900, webview2.HintNone)
mustBind(w, "getState", a.GetState)
mustBind(w, "getEditState", a.GetEditState)
mustBind(w, "connect", a.Connect)
mustBind(w, "disconnect", a.Disconnect)
mustBind(w, "connectProfile", a.ConnectProfile)
mustBind(w, "saveProfile", a.SaveProfile)
mustBind(w, "createProfile", a.CreateProfile)
mustBind(w, "selectProfile", a.SelectProfile)
mustBind(w, "deleteProfile", a.DeleteProfile)
mustBind(w, "installCore", a.InstallCore)
mustBind(w, "openURL", a.OpenShopURL)
mustBind(w, "pingServers", a.PingServers)
mustBind(w, "pingBest", a.PingBest)
mustBind(w, "checkUpdate", a.CheckUpdate)
mustBind(w, "applyUpdate", a.ApplyUpdate)
mustBind(w, "saveHy2", a.SaveHy2)
mustBind(w, "importSubscription", a.ImportSubscription)
mustBind(w, "getLogs", a.GetLogs)
mustBind(w, "probeTunnel", a.ProbeTunnel)
mustBind(w, "savePrefs", a.SavePrefs)
a.StartBackground()
trayhost.Start("Navis", trayhost.Hooks{
Connect: func() { _ = a.Connect() },
Disconnect: func() { _ = a.Disconnect() },
Quit: func() {
_ = a.Disconnect()
os.Exit(0)
},
IsUp: func() bool { return a.Mgr.Status().Connected },
})
go a.AutoCheckUpdate()
w.SetHtml(appui.IndexHTML)
w.Run()
}
func mustBind(w webview2.WebView, name string, fn interface{}) {
if err := w.Bind(name, fn); err != nil {
fatalDialog("bind %s: %v", name, err)
}
}
func shellOpen(raw string) error {
verb, err := windows.UTF16PtrFromString("open")
if err != nil {
return err
}
file, err := windows.UTF16PtrFromString(raw)
if err != nil {
return err
}
return windows.ShellExecute(0, verb, file, nil, nil, windows.SW_SHOWNORMAL)
}
func applyWindowIcon(hwnd unsafe.Pointer) {
ico := findIconPath()
if ico == "" || hwnd == nil {
return
}
pathPtr, err := windows.UTF16PtrFromString(ico)
if err != nil {
return
}
user32 := windows.NewLazySystemDLL("user32.dll")
loadImage := user32.NewProc("LoadImageW")
sendMessage := user32.NewProc("SendMessageW")
const (
imageIcon = 1
lrLoadFromFile = 0x0010
lrDefaultSize = 0x0040
wmSetIcon = 0x0080
iconSmall = 0
iconBig = 1
)
h, _, _ := loadImage.Call(0, uintptr(unsafe.Pointer(pathPtr)), imageIcon, 0, 0, lrLoadFromFile|lrDefaultSize)
if h == 0 {
return
}
sendMessage.Call(uintptr(hwnd), wmSetIcon, iconSmall, h)
sendMessage.Call(uintptr(hwnd), wmSetIcon, iconBig, h)
}
func findIconPath() string {
candidates := []string{}
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
candidates = append(candidates,
filepath.Join(dir, "assets", "navis.ico"),
filepath.Join(dir, "navis.ico"),
)
}
if cwd, err := os.Getwd(); err == nil {
candidates = append(candidates, filepath.Join(cwd, "assets", "navis.ico"))
}
for _, c := range candidates {
if st, err := os.Stat(c); err == nil && !st.IsDir() {
return c
}
}
return ""
}
func fatalDialog(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
log.Println(msg)
messageBox(msg)
os.Exit(1)
}
+33
View File
@@ -0,0 +1,33 @@
//go:build darwin
package main
import (
"fmt"
"log"
"os"
"os/exec"
"syscall"
"github.com/crgimenes/glaze"
)
func openExternal(raw string) error {
return exec.Command("open", raw).Start()
}
func fatalf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
log.Println(msg)
_ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "Navis" message %q`, msg)).Run()
os.Exit(1)
}
func shutdownSignals() []os.Signal {
return []os.Signal{os.Interrupt, syscall.SIGTERM}
}
func decorateWindow(w glaze.WebView) {
// macOS Dock/app icon comes from the .app bundle; nothing to apply here.
_ = w
}
+92
View File
@@ -0,0 +1,92 @@
//go:build windows
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"unsafe"
"github.com/crgimenes/glaze"
"golang.org/x/sys/windows"
)
func openExternal(raw string) error {
verb, err := windows.UTF16PtrFromString("open")
if err != nil {
return err
}
file, err := windows.UTF16PtrFromString(raw)
if err != nil {
return err
}
return windows.ShellExecute(0, verb, file, nil, nil, windows.SW_SHOWNORMAL)
}
func fatalf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
log.Println(msg)
messageBox(msg)
os.Exit(1)
}
func shutdownSignals() []os.Signal {
return []os.Signal{os.Interrupt}
}
func decorateWindow(w glaze.WebView) {
if w == nil {
return
}
applyWindowIcon(w.Window())
}
func applyWindowIcon(hwnd unsafe.Pointer) {
ico := findIconPath()
if ico == "" || hwnd == nil {
return
}
pathPtr, err := windows.UTF16PtrFromString(ico)
if err != nil {
return
}
user32 := windows.NewLazySystemDLL("user32.dll")
loadImage := user32.NewProc("LoadImageW")
sendMessage := user32.NewProc("SendMessageW")
const (
imageIcon = 1
lrLoadFromFile = 0x0010
lrDefaultSize = 0x0040
wmSetIcon = 0x0080
iconSmall = 0
iconBig = 1
)
h, _, _ := loadImage.Call(0, uintptr(unsafe.Pointer(pathPtr)), imageIcon, 0, 0, lrLoadFromFile|lrDefaultSize)
if h == 0 {
return
}
sendMessage.Call(uintptr(hwnd), wmSetIcon, iconSmall, h)
sendMessage.Call(uintptr(hwnd), wmSetIcon, iconBig, h)
}
func findIconPath() string {
candidates := []string{}
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
candidates = append(candidates,
filepath.Join(dir, "assets", "navis.ico"),
filepath.Join(dir, "navis.ico"),
)
}
if cwd, err := os.Getwd(); err == nil {
candidates = append(candidates, filepath.Join(cwd, "assets", "navis.ico"))
}
for _, c := range candidates {
if st, err := os.Stat(c); err == nil && !st.IsDir() {
return c
}
}
return ""
}
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -5,12 +5,12 @@
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
"os": "windows",
"arch": "amd64"
},
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -5,12 +5,12 @@
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
"os": "windows",
"arch": "amd64"
},
+2 -4
View File
@@ -4,21 +4,19 @@ go 1.26.5
require (
github.com/amnezia-vpn/amneziawg-go v0.2.19
github.com/crgimenes/glaze v0.0.33
github.com/diskfs/go-diskfs v1.9.4
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808
github.com/ebitengine/purego v0.10.1
golang.org/x/image v0.44.0
golang.org/x/sys v0.43.0
)
require (
github.com/anchore/go-lzo v0.1.0 // indirect
github.com/crgimenes/glaze v0.0.33 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/elliotwutingfeng/asciiset v0.0.0-20260129054604-cfde2086bc57 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/pkg/xattr v0.4.12 // indirect
-6
View File
@@ -22,10 +22,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 h1:ftnsTqIUH57XQEF+PnXX9++nlHCzdkuB5zbWyMMruZo=
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808/go.mod h1:rWifBlzkgrvd7zUqlfq91sWt3473OikgnglnIILx/Jo=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
@@ -48,8 +44,6 @@ golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+68
View File
@@ -0,0 +1,68 @@
package apphost
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"vpnclient/internal/config"
"vpnclient/internal/core"
"vpnclient/internal/logbuf"
)
func TestHTTPBridgeGetState(t *testing.T) {
cfg := &config.Config{
Active: "demo",
Profiles: []config.Profile{
{Name: "demo", Protocol: config.ProtocolNaive, Proxy: "https://u:p@example.com"},
},
}
mgr, err := core.NewManager("", cfg, logbuf.New(0))
if err != nil {
t.Fatal(err)
}
a := New(mgr, "", logbuf.New(0))
a.APIToken = "test-token"
body, _ := json.Marshal(map[string]any{"args": []any{}})
req := httptest.NewRequest(http.MethodPost, "/api/getState", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Navis-Token", "test-token")
rr := httptest.NewRecorder()
a.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status %d body %s", rr.Code, rr.Body.String())
}
var resp struct {
Result UIState `json:"result"`
Error string `json:"error"`
}
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp.Error != "" {
t.Fatalf("api error: %s", resp.Error)
}
if resp.Result.ActiveProfile != "demo" {
t.Fatalf("active_profile=%q", resp.Result.ActiveProfile)
}
}
func TestHTTPBridgeRejectsBadToken(t *testing.T) {
cfg := &config.Config{}
mgr, err := core.NewManager("", cfg, logbuf.New(0))
if err != nil {
t.Fatal(err)
}
a := New(mgr, "", logbuf.New(0))
a.APIToken = "secret"
req := httptest.NewRequest(http.MethodPost, "/api/getState", bytes.NewReader([]byte(`{"args":[]}`)))
req.Header.Set("X-Navis-Token", "wrong")
rr := httptest.NewRecorder()
a.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusUnauthorized {
t.Fatalf("want 401, got %d", rr.Code)
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ var appCSS string
//go:embed app.js
var appJS string
// IndexHTML is the full GUI document (CSS/JS inlined for WebView2 / glaze).
// IndexHTML is the full GUI document (CSS/JS inlined for glaze HTTP UI).
var IndexHTML = buildIndexHTML()
func buildIndexHTML() string {
+1 -1
View File
@@ -22,7 +22,7 @@ const CurrentVersion = "3.8.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 = 3
const BuildNumber = 4
// 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"
+1 -1
View File
@@ -6,7 +6,7 @@ cd "$(dirname "$0")/.."
# Prefer local toolchain if present.
export PATH="$(pwd)/.tools/go/bin:/tmp/go-sdk/go/bin:/usr/local/go/bin:$PATH"
python3 scripts/sync-version.py
python3 scripts/bump-build.py
VERSION="$(python3 - <<'PY'
import re
+59
View File
@@ -0,0 +1,59 @@
# Increment BuildNumber in update.go and sync version artifacts (no Python required).
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot
$updateGo = Join-Path $Root "internal\update\update.go"
$text = [IO.File]::ReadAllText($updateGo)
$m = [regex]::Match($text, 'const BuildNumber\s*=\s*(\d+)')
if (-not $m.Success) { throw "cannot find BuildNumber in update.go" }
$old = [int]$m.Groups[1].Value
$new = $old + 1
$text2 = [regex]::Replace($text, 'const BuildNumber\s*=\s*\d+', "const BuildNumber = $new", 1)
[IO.File]::WriteAllText($updateGo, $text2)
Write-Host "BuildNumber: $old -> $new"
$verM = [regex]::Match($text2, 'CurrentVersion\s*=\s*"([^"]+)"')
if (-not $verM.Success) { throw "cannot find CurrentVersion" }
$ver = $verM.Groups[1].Value
$parts = @($ver.Split('.') | ForEach-Object { [int]$_ })
while ($parts.Count -lt 3) { $parts += 0 }
$major, $minor, $patch = $parts[0], $parts[1], $parts[2]
$full = "$ver.$new"
$display = "$ver+$new"
$versionCode = $major * 1000000 + $minor * 10000 + $patch * 100 + $new
$viPath = Join-Path $Root "versioninfo.json"
$vi = [IO.File]::ReadAllText($viPath)
$vi = [regex]::Replace($vi, '("FileVersion"\s*:\s*\{\s*"Major"\s*:\s*)\d+', "`${1}$major", 1)
$vi = [regex]::Replace($vi, '("Minor"\s*:\s*)\d+(\s*,\s*"Patch")', "`${1}$minor`${2}", 1)
$vi = [regex]::Replace($vi, '("Patch"\s*:\s*)\d+(\s*,\s*"Build")', "`${1}$patch`${2}", 1)
$vi = [regex]::Replace($vi, '("Build"\s*:\s*)\d+', "`${1}$new", 2)
$vi = [regex]::Replace($vi, '("FileVersion"\s*:\s*")\d+\.\d+\.\d+\.\d+(")', "`${1}$full`${2}")
$vi = [regex]::Replace($vi, '("ProductVersion"\s*:\s*")\d+\.\d+\.\d+\.\d+(")', "`${1}$full`${2}")
[IO.File]::WriteAllText($viPath, $vi)
Write-Host "updated versioninfo.json -> $full"
$batPath = Join-Path $Root "build-macos.bat"
$bat = [IO.File]::ReadAllText($batPath)
$bat2 = [regex]::Replace(
$bat,
'-version\s+\d+\.\d+\.\d+\s+-build\s+\d+\.\d+\.\d+\.\d+',
"-version $ver -build $full"
)
if ($bat2 -ne $bat) {
[IO.File]::WriteAllText($batPath, $bat2)
Write-Host "updated build-macos.bat -> $ver $full"
}
$gradlePath = Join-Path $Root "android\app\build.gradle.kts"
if (Test-Path $gradlePath) {
$g = [IO.File]::ReadAllText($gradlePath)
$g2 = [regex]::Replace($g, 'versionCode\s*=\s*[\d_]+', "versionCode = $versionCode")
$g2 = [regex]::Replace($g2, 'versionName\s*=\s*"[^"]+"', "versionName = `"$display`"")
if ($g2 -ne $g) {
[IO.File]::WriteAllText($gradlePath, $g2)
Write-Host "updated android gradle -> $display $versionCode"
}
}
Write-Host "bump complete: $full"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Increment BuildNumber in update.go, then sync versioninfo / macOS bat / Android."""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
UPDATE_GO = ROOT / "internal" / "update" / "update.go"
def bump() -> int:
text = UPDATE_GO.read_text(encoding="utf-8")
m = re.search(r"const BuildNumber\s*=\s*(\d+)", text)
if not m:
raise SystemExit("cannot find BuildNumber in update.go")
n = int(m.group(1)) + 1
text2, count = re.subn(
r"const BuildNumber\s*=\s*\d+",
f"const BuildNumber = {n}",
text,
count=1,
)
if count != 1:
raise SystemExit("failed to rewrite BuildNumber")
UPDATE_GO.write_text(text2, encoding="utf-8")
print(f"BuildNumber: {m.group(1)} -> {n}", flush=True)
return n
def main() -> None:
bump()
sync = ROOT / "scripts" / "sync-version.py"
r = subprocess.run([sys.executable, str(sync)], cwd=ROOT, check=False)
if r.returncode != 0:
raise SystemExit(r.returncode)
if __name__ == "__main__":
main()
+2 -2
View File
@@ -5,12 +5,12 @@
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e",
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
"os": "windows",
"arch": "amd64"
},
+4 -4
View File
@@ -4,13 +4,13 @@
"Major": 3,
"Minor": 8,
"Patch": 2,
"Build": 3
"Build": 4
},
"ProductVersion": {
"Major": 3,
"Minor": 8,
"Patch": 2,
"Build": 3
"Build": 4
},
"FileFlagsMask": "3f",
"FileFlags": "00",
@@ -21,12 +21,12 @@
"StringFileInfo": {
"CompanyName": "EvilFox",
"FileDescription": "Navis — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
"FileVersion": "3.8.2.3",
"FileVersion": "3.8.2.4",
"InternalName": "Navis",
"LegalCopyright": "Copyright (c) EvilFox",
"OriginalFilename": "Navis.exe",
"ProductName": "Navis",
"ProductVersion": "3.8.2.3",
"ProductVersion": "3.8.2.4",
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
},
"VarFileInfo": {