Add WebView GUI to Windows installer (#5532)

* Add WebView GUI to Windows installer

Adds a native GUI window to the Windows installer using the webview
library which embeds Microsoft Edge WebView2. The installer now shows
a modern HTML/CSS/JS interface with progress bar and status updates.

Key changes:
- Add llvm-mingw toolchain for macOS → Windows CGO cross-compilation
- Add webview_go dependency for WebView2 integration
- Create UIInterface abstraction with ConsoleUI and WebViewUI impls
- Build tags select stub (non-Windows/non-CGO) vs real WebView impl
- Genrule handles CGO cross-compilation within Bazel sandbox

Build targets:
- eagle_installer_windows_amd64: Pure Go console version (5.8 MB)
- eagle_installer_windows_amd64_webview: CGO WebView GUI (13.7 MB)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix WebView genrule to use Bazel Go SDK

The CI doesn't have Go in PATH, so the genrule needs to use Bazel's
Go SDK explicitly. This change:

- Exposes @go_default_sdk via use_repo in MODULE.bazel
- Adds @go_default_sdk//:files as a srcs dependency to the genrule
- Uses absolute paths for GO and GOROOT so they work after cd
- Filters out Go SDK files when copying source files
- Updates go.mod version to 1.23 to match the Bazel SDK

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 19:36:21 -08:00
committed by GitHub
co-authored by Claude Opus 4.5
parent 420edb15ae
commit 12bd6a78b1
12 changed files with 1173 additions and 29 deletions
+15
View File
@@ -107,6 +107,7 @@ bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle")
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.23.3")
use_repo(go_sdk, "go_default_sdk")
go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
@@ -118,6 +119,7 @@ use_repo(
"com_github_aws_aws_sdk_go_v2_service_s3",
"com_github_golang_jwt_jwt_v5",
"com_github_google_uuid",
"com_github_webview_webview_go",
"org_golang_google_grpc",
"org_golang_google_protobuf",
)
@@ -336,6 +338,19 @@ http_file(
executable = True,
)
# LLVM MinGW toolchain for Windows cross-compilation from macOS
# This provides a complete toolchain for building Windows executables including
# the MinGW-w64 libraries needed for CGO cross-compilation
LLVM_MINGW_VERSION = "20250305"
http_archive(
name = "llvm_mingw",
build_file = "@//external:BUILD.llvm_mingw",
sha256 = "32c24fc62fc8b9f8a900bf2c730b78b36767688f816f9d21e97a168289ff44e0",
strip_prefix = "llvm-mingw-%s-ucrt-macos-14.4.1-universal" % LLVM_MINGW_VERSION,
urls = ["https://github.com/fathonix/llvm-mingw-arm64ec-macos/releases/download/%s/llvm-mingw-%s-ucrt-macos-14.4.1-universal.tar.xz" % (LLVM_MINGW_VERSION, LLVM_MINGW_VERSION)],
)
#
# Toolchain Registration
#
+561
View File
@@ -611,6 +611,445 @@
]
}
},
"@@aspect_rules_js~//npm:extensions.bzl%pnpm": {
"general": {
"bzlTransitiveDigest": "T22SdPzhLxF1CM+j9RD/Rq03yJ0NDfH2eE6hAbUcOII=",
"usagesDigest": "6rWte4KDbiluq1s7w98bc4+2NjA8w67DKHDj4+DNw/Y=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"pnpm": {
"bzlFile": "@@aspect_rules_js~//npm/private:npm_import.bzl",
"ruleClassName": "npm_import_rule",
"attributes": {
"package": "pnpm",
"version": "8.6.7",
"root_package": "",
"link_workspace": "",
"link_packages": {},
"integrity": "sha512-vRIWpD/L4phf9Bk2o/O2TDR8fFoJnpYrp2TKqTIZF/qZ2/rgL3qKXzHofHgbXsinwMoSEigz28sqk3pQ+yMEQQ==",
"url": "",
"commit": "",
"patch_args": [
"-p0"
],
"patches": [],
"custom_postinstall": "",
"npm_auth": "",
"npm_auth_basic": "",
"npm_auth_username": "",
"npm_auth_password": "",
"lifecycle_hooks": [],
"extra_build_content": "load(\"@aspect_rules_js//js:defs.bzl\", \"js_binary\")\njs_binary(name = \"pnpm\", data = glob([\"package/**\"]), entry_point = \"package/dist/pnpm.cjs\", visibility = [\"//visibility:public\"])",
"generate_bzl_library_targets": false,
"extract_full_archive": true,
"exclude_package_contents": [],
"system_tar": "auto"
}
},
"pnpm__links": {
"bzlFile": "@@aspect_rules_js~//npm/private:npm_import.bzl",
"ruleClassName": "npm_import_links",
"attributes": {
"package": "pnpm",
"version": "8.6.7",
"dev": false,
"root_package": "",
"link_packages": {},
"deps": {},
"transitive_closure": {},
"lifecycle_build_target": false,
"lifecycle_hooks_env": [],
"lifecycle_hooks_execution_requirements": [
"no-sandbox"
],
"lifecycle_hooks_use_default_shell_env": false,
"bins": {},
"package_visibility": [
"//visibility:public"
],
"replace_package": "",
"exclude_package_contents": []
}
}
},
"recordedRepoMappingEntries": [
[
"aspect_bazel_lib~",
"bazel_lib",
"bazel_lib~"
],
[
"aspect_bazel_lib~",
"bazel_skylib",
"bazel_skylib~"
],
[
"aspect_bazel_lib~",
"bazel_tools",
"bazel_tools"
],
[
"aspect_bazel_lib~",
"tar.bzl",
"tar.bzl~"
],
[
"aspect_rules_js~",
"aspect_bazel_lib",
"aspect_bazel_lib~"
],
[
"aspect_rules_js~",
"aspect_rules_js",
"aspect_rules_js~"
],
[
"aspect_rules_js~",
"bazel_features",
"bazel_features~"
],
[
"aspect_rules_js~",
"bazel_skylib",
"bazel_skylib~"
],
[
"aspect_rules_js~",
"bazel_tools",
"bazel_tools"
],
[
"bazel_features~",
"bazel_features_globals",
"bazel_features~~version_extension~bazel_features_globals"
],
[
"bazel_features~",
"bazel_features_version",
"bazel_features~~version_extension~bazel_features_version"
],
[
"bazel_lib~",
"bazel_skylib",
"bazel_skylib~"
],
[
"bazel_lib~",
"bazel_tools",
"bazel_tools"
],
[
"tar.bzl~",
"aspect_bazel_lib",
"aspect_bazel_lib~"
],
[
"tar.bzl~",
"bazel_skylib",
"bazel_skylib~"
],
[
"tar.bzl~",
"tar.bzl",
"tar.bzl~"
]
]
}
},
"@@aspect_rules_ts~//ts:extensions.bzl%ext": {
"general": {
"bzlTransitiveDigest": "h1hftyFCdJgiHD9blfFcMAiEk5ltEoUdNkuHPIkg/hM=",
"usagesDigest": "v0aTa4/gasWF2bvssXYr1bqcYWc3kjV48hcj0z2QVT0=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"npm_typescript": {
"bzlFile": "@@aspect_rules_ts~//ts/private:npm_repositories.bzl",
"ruleClassName": "http_archive_version",
"attributes": {
"bzlmod": true,
"version": "5.8.3",
"integrity": "",
"build_file": "@@aspect_rules_ts~//ts:BUILD.typescript",
"build_file_substitutions": {
"bazel_worker_version": "5.4.2",
"google_protobuf_version": "3.20.1"
},
"urls": [
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
]
}
}
},
"recordedRepoMappingEntries": [
[
"aspect_rules_ts~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@cel-spec~//:extensions.bzl%non_module_dependencies": {
"general": {
"bzlTransitiveDigest": "/2uyuQa5purSharolRXYOGYMGSGjDeByo6JfQDWsraA=",
"usagesDigest": "2f6juplOpWu+UdD1kVgi773xavnFQ+OcH0PRuQduDxY=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"com_google_googleapis": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "bd8e735d881fb829751ecb1a77038dda4a8d274c45490cb9fcf004583ee10571",
"strip_prefix": "googleapis-07c27163ac591955d736f3057b1619ece66f5b99",
"urls": [
"https://github.com/googleapis/googleapis/archive/07c27163ac591955d736f3057b1619ece66f5b99.tar.gz"
]
}
}
},
"recordedRepoMappingEntries": [
[
"cel-spec~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@cel-spec~//:googleapis_ext.bzl%googleapis_ext": {
"general": {
"bzlTransitiveDigest": "yun2jmsomFi3bs5bjQWXApBzqQf66zBJ39JEBYigzdc=",
"usagesDigest": "OK8FsLndSl2AbwGM1Npe5NdHR1kDAebcw7Ee+KkekE0=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"com_google_googleapis_imports": {
"bzlFile": "@@cel-spec~~non_module_dependencies~com_google_googleapis//:repository_rules.bzl",
"ruleClassName": "switched_rules",
"attributes": {
"rules": {
"proto_library_with_info": [
"",
""
],
"moved_proto_library": [
"",
""
],
"java_proto_library": [
"",
""
],
"java_grpc_library": [
"",
""
],
"java_gapic_library": [
"",
""
],
"java_gapic_test": [
"",
""
],
"java_gapic_assembly_gradle_pkg": [
"",
""
],
"py_proto_library": [
"",
""
],
"py_grpc_library": [
"",
""
],
"py_gapic_library": [
"",
""
],
"py_test": [
"",
""
],
"py_gapic_assembly_pkg": [
"",
""
],
"py_import": [
"",
""
],
"go_proto_library": [
"",
""
],
"go_library": [
"",
""
],
"go_test": [
"",
""
],
"go_gapic_library": [
"",
""
],
"go_gapic_assembly_pkg": [
"",
""
],
"cc_proto_library": [
"native.cc_proto_library",
""
],
"cc_grpc_library": [
"",
""
],
"cc_gapic_library": [
"",
""
],
"php_proto_library": [
"",
"php_proto_library"
],
"php_grpc_library": [
"",
"php_grpc_library"
],
"php_gapic_library": [
"",
"php_gapic_library"
],
"php_gapic_assembly_pkg": [
"",
"php_gapic_assembly_pkg"
],
"nodejs_gapic_library": [
"",
"typescript_gapic_library"
],
"nodejs_gapic_assembly_pkg": [
"",
"typescript_gapic_assembly_pkg"
],
"ruby_proto_library": [
"",
""
],
"ruby_grpc_library": [
"",
""
],
"ruby_ads_gapic_library": [
"",
""
],
"ruby_cloud_gapic_library": [
"",
""
],
"ruby_gapic_assembly_pkg": [
"",
""
],
"csharp_proto_library": [
"",
""
],
"csharp_grpc_library": [
"",
""
],
"csharp_gapic_library": [
"",
""
],
"csharp_gapic_assembly_pkg": [
"",
""
]
}
}
}
},
"recordedRepoMappingEntries": [
[
"cel-spec~",
"com_google_googleapis",
"cel-spec~~non_module_dependencies~com_google_googleapis"
]
]
}
},
"@@envoy_api~//bazel:repositories.bzl%non_module_deps": {
"general": {
"bzlTransitiveDigest": "6TqmRfVELxZJRPQYuJpC4JBX4QvdrHqTDOUJGOGODSo=",
"usagesDigest": "IivjlawPvqhHPUJ3c6dLiPsH22mn/g/dJtlmv3zdimM=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"prometheus_metrics_model": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/prometheus/client_model/archive/v0.6.1.tar.gz"
],
"sha256": "b9b690bc35d80061f255faa7df7621eae39fe157179ccd78ff6409c3b004f05e",
"strip_prefix": "client_model-0.6.1",
"build_file_content": "\nload(\"@envoy_api//bazel:api_build_system.bzl\", \"api_cc_py_proto_library\")\nload(\"@io_bazel_rules_go//proto:def.bzl\", \"go_proto_library\")\n\napi_cc_py_proto_library(\n name = \"client_model\",\n srcs = [\n \"io/prometheus/client/metrics.proto\",\n ],\n visibility = [\"//visibility:public\"],\n)\n\ngo_proto_library(\n name = \"client_model_go_proto\",\n importpath = \"github.com/prometheus/client_model/go\",\n proto = \":client_model\",\n visibility = [\"//visibility:public\"],\n)\n"
}
},
"com_github_bufbuild_buf": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/bufbuild/buf/releases/download/v1.49.0/buf-Linux-x86_64.tar.gz"
],
"sha256": "ee8da9748249f7946d79191e36469ce7bc3b8ba80019bff1fa4289a44cbc23bf",
"strip_prefix": "buf",
"build_file_content": "\npackage(\n default_visibility = [\"//visibility:public\"],\n)\n\nfilegroup(\n name = \"buf\",\n srcs = [\n \"@com_github_bufbuild_buf//:bin/buf\",\n ],\n tags = [\"manual\"], # buf is downloaded as a linux binary; tagged manual to prevent build for non-linux users\n)\n"
}
},
"envoy_toolshed": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/envoyproxy/toolshed/archive/bazel-v0.2.2.tar.gz"
],
"sha256": "443fe177aba0cef8c17b7a48905c925c67b09005b10dd70ff12cd9f729a72d51",
"strip_prefix": "toolshed-bazel-v0.2.2/bazel"
}
}
},
"recordedRepoMappingEntries": [
[
"envoy_api~",
"bazel_tools",
"bazel_tools"
],
[
"envoy_api~",
"envoy_api",
"envoy_api~"
]
]
}
},
"@@googleapis~//:extensions.bzl%switched_rules": {
"general": {
"bzlTransitiveDigest": "vG6fuTzXD8MMvHWZEQud0MMH7eoC4GXY0va7VrFFh04=",
@@ -779,6 +1218,37 @@
"recordedRepoMappingEntries": []
}
},
"@@pybind11_bazel~//:internal_configure.bzl%internal_configure_extension": {
"general": {
"bzlTransitiveDigest": "CyAKLVVonohnkTSqg9II/HA7M49sOlnMkgMHL3CmDuc=",
"usagesDigest": "mFrTHX5eCiNU/OIIGVHH3cOILY9Zmjqk8RQYv8o6Thk=",
"recordedFileInputs": {
"@@pybind11_bazel~//MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34"
},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"pybind11": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file": "@@pybind11_bazel~//:pybind11-BUILD.bazel",
"strip_prefix": "pybind11-2.12.0",
"urls": [
"https://github.com/pybind/pybind11/archive/v2.12.0.zip"
]
}
}
},
"recordedRepoMappingEntries": [
[
"pybind11_bazel~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_foreign_cc~//foreign_cc:extensions.bzl%tools": {
"general": {
"bzlTransitiveDigest": "a7qnESofmIRYId6wwGNPJ9kvExU80KrkxL281P3+lBE=",
@@ -1119,6 +1589,97 @@
]
}
},
"@@rules_fuzzing~//fuzzing/private:extensions.bzl%non_module_dependencies": {
"general": {
"bzlTransitiveDigest": "hVgJRQ3Er45/UUAgNn1Yp2Khcp/Y8WyafA2kXIYmQ5M=",
"usagesDigest": "YnIrdgwnf3iCLfChsltBdZ7yOJh706lpa2vww/i2pDI=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"platforms": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz",
"https://github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz"
],
"sha256": "8150406605389ececb6da07cbcb509d5637a3ab9a24bc69b1101531367d89d74"
}
},
"rules_python": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "d70cd72a7a4880f0000a6346253414825c19cdd40a28289bdf67b8e6480edff8",
"strip_prefix": "rules_python-0.28.0",
"url": "https://github.com/bazelbuild/rules_python/releases/download/0.28.0/rules_python-0.28.0.tar.gz"
}
},
"bazel_skylib": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "cd55a062e763b9349921f0f5db8c3933288dc8ba4f76dd9416aac68acee3cb94",
"urls": [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz",
"https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz"
]
}
},
"com_google_absl": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/abseil/abseil-cpp/archive/refs/tags/20240116.1.zip"
],
"strip_prefix": "abseil-cpp-20240116.1",
"integrity": "sha256-7capMWOvWyoYbUaHF/b+I2U6XLMaHmky8KugWvfXYuk="
}
},
"rules_fuzzing_oss_fuzz": {
"bzlFile": "@@rules_fuzzing~//fuzzing/private/oss_fuzz:repository.bzl",
"ruleClassName": "oss_fuzz_repository",
"attributes": {}
},
"honggfuzz": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file": "@@rules_fuzzing~//:honggfuzz.BUILD",
"sha256": "6b18ba13bc1f36b7b950c72d80f19ea67fbadc0ac0bb297ec89ad91f2eaa423e",
"url": "https://github.com/google/honggfuzz/archive/2.5.zip",
"strip_prefix": "honggfuzz-2.5"
}
},
"rules_fuzzing_jazzer": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_jar",
"attributes": {
"sha256": "ee6feb569d88962d59cb59e8a31eb9d007c82683f3ebc64955fd5b96f277eec2",
"url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer/0.20.1/jazzer-0.20.1.jar"
}
},
"rules_fuzzing_jazzer_api": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_jar",
"attributes": {
"sha256": "f5a60242bc408f7fa20fccf10d6c5c5ea1fcb3c6f44642fec5af88373ae7aa1b",
"url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer-api/0.20.1/jazzer-api-0.20.1.jar"
}
}
},
"recordedRepoMappingEntries": [
[
"rules_fuzzing~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_java~//java:rules_java_deps.bzl%compatibility_proxy": {
"general": {
"bzlTransitiveDigest": "KIX40nDfygEWbU+rq3nYpt3tVgTK/iO8PKh5VMBlN7M=",
+48
View File
@@ -0,0 +1,48 @@
# LLVM MinGW toolchain for Windows cross-compilation
# Provides x86_64-w64-mingw32 target compiler and libraries
package(default_visibility = ["//visibility:public"])
filegroup(
name = "all_files",
srcs = glob(["**/*"]),
)
# Compiler binaries
filegroup(
name = "compiler_files",
srcs = glob([
"bin/x86_64-w64-mingw32-*",
"bin/clang*",
"bin/llvm-*",
"bin/lld*",
]),
)
# Windows x86_64 sysroot (headers and libraries)
filegroup(
name = "windows_x86_64_sysroot",
srcs = glob([
"x86_64-w64-mingw32/**/*",
"generic-w64-mingw32/include/**/*",
]),
)
# All library files needed for linking
filegroup(
name = "linker_files",
srcs = glob([
"bin/x86_64-w64-mingw32-*",
"bin/lld*",
"bin/ld.lld*",
"lib/**/*",
"x86_64-w64-mingw32/lib/**/*",
]),
)
# The main C compiler wrapper script path for CGO
# CGO needs CC to point to the cross-compiler
exports_files([
"bin/x86_64-w64-mingw32-clang",
"bin/x86_64-w64-mingw32-clang++",
])
+1
View File
@@ -11,6 +11,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.36.3
)
+2
View File
@@ -41,6 +41,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1,21 +1,29 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
# Pure Go library (uses stub for WebView)
go_library(
name = "installer_lib",
srcs = [
"config.go",
"main.go",
"ui.go",
"ui_interface.go",
"ui_webview_stub.go",
"ui_webview_windows.go",
"updater.go",
],
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/clients/win/installer",
visibility = ["//visibility:private"],
deps = select({
"@io_bazel_rules_go//go/platform:windows": [
"@com_github_webview_webview_go//:webview_go",
],
"//conditions:default": [],
}),
)
# Windows x86_64 installer binary
# Windows x86_64 installer binary (pure Go, console-only)
# Build with: bazel build //src/main/go/net/eagle0/clients/win/installer:eagle_installer_windows_amd64
# The manifest public key is injected at build time via x_defs
# The resource_windows.syso file embeds the application icon and version info
go_binary(
name = "eagle_installer_windows_amd64",
srcs = ["resource_windows.syso"],
@@ -30,7 +38,143 @@ go_binary(
},
)
# Native binary for testing on current platform
# Windows x86_64 installer with WebView GUI
# Build with: bazel build //src/main/go/net/eagle0/clients/win/installer:eagle_installer_windows_amd64_webview
# This uses CGO cross-compilation with llvm-mingw toolchain
genrule(
name = "eagle_installer_windows_amd64_webview",
srcs = [
"config.go",
"main.go",
"ui.go",
"ui_interface.go",
"ui_webview_windows.go",
"updater.go",
"resource_windows.syso",
"@llvm_mingw//:all_files",
"@go_default_sdk//:files",
],
outs = ["EagleInstallerWebView.exe"],
cmd = """
set -e
# Find llvm-mingw root
LLVM_MINGW=""
for f in $(locations @llvm_mingw//:all_files); do
if [[ "$$f" == *"/bin/x86_64-w64-mingw32-clang" ]]; then
LLVM_MINGW=$$(cd $$(dirname $$(dirname $$f)) && pwd)
break
fi
done
if [[ -z "$$LLVM_MINGW" ]]; then
echo "ERROR: Could not find llvm-mingw toolchain"
exit 1
fi
echo "Using llvm-mingw at: $$LLVM_MINGW"
# Find Go SDK from Bazel dependency - use absolute paths
GO=""
GO_SDK=""
SANDBOX_ROOT="$$PWD"
for f in $(locations @go_default_sdk//:files); do
if [[ "$$f" == */bin/go && ! "$$f" == *.* ]]; then
GO="$$SANDBOX_ROOT/$$f"
GO_SDK=$$(cd $$(dirname $$(dirname "$$SANDBOX_ROOT/$$f")) && pwd)
break
fi
done
if [[ -z "$$GO" ]]; then
echo "ERROR: Could not find Go binary in @go_default_sdk//:files"
echo "Available files:"
for f in $(locations @go_default_sdk//:files); do
echo " $$f"
done | head -20
exit 1
fi
export GOROOT="$$GO_SDK"
echo "Using Go SDK at: $$GO_SDK"
$$GO version
# Set up cross-compilation environment
export CGO_ENABLED=1
export GOOS=windows
export GOARCH=amd64
export CC="$$LLVM_MINGW/bin/x86_64-w64-mingw32-clang"
export CXX="$$LLVM_MINGW/bin/x86_64-w64-mingw32-clang++"
export AR="$$LLVM_MINGW/bin/x86_64-w64-mingw32-ar"
# Verify cross-compiler works
echo "Testing cross-compiler..."
$$CC --version | head -1
# Create temp build directory
BUILD_DIR=$$(mktemp -d)
trap "chmod -R u+w $$BUILD_DIR 2>/dev/null; rm -rf $$BUILD_DIR" EXIT
# Set up Go environment variables for sandbox
export HOME=$$BUILD_DIR
export GOPATH=$$BUILD_DIR/go
export GOCACHE=$$BUILD_DIR/cache
mkdir -p $$GOPATH $$GOCACHE
# Create Go module structure
mkdir -p $$BUILD_DIR/installer
cd $$BUILD_DIR/installer
# Initialize go module
cat > go.mod << 'GOMOD'
module github.com/nolen777/eagle0/src/main/go/net/eagle0/clients/win/installer
go 1.23
require github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
GOMOD
cat > go.sum << 'GOSUM'
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=
GOSUM
# Copy source files from Bazel sandbox (exclude Go SDK and llvm files)
echo "Copying source files from $$SANDBOX_ROOT..."
for src in $(SRCS); do
# Skip Go SDK and toolchain files
if [[ "$$src" == *"go_default_sdk"* || "$$src" == *"llvm_mingw"* || "$$src" == *"rules_go"* ]]; then
continue
fi
case "$$src" in
*.go)
echo " Copying: $$src"
cp "$$SANDBOX_ROOT/$$src" . || { echo "Failed to copy $$src"; exit 1; }
;;
# Skip .syso for now - the existing one is 32-bit and causes link errors
# TODO: Generate 64-bit .syso resource file
esac
done
echo "Files copied:"
ls -la *.go 2>/dev/null || echo "No files copied!"
# Download webview dependency
$$GO mod download
# Build with GUI subsystem flag
echo "Building Windows installer with WebView..."
OUTPUT_EXE="$$SANDBOX_ROOT/$@"
$$GO build -v \\
-ldflags="-H windowsgui -X main.manifestPublicKey={MANIFEST_PUBLIC_KEY}" \\
-o "$$OUTPUT_EXE" \\
.
echo "Build complete!"
ls -la "$$OUTPUT_EXE"
""",
visibility = ["//visibility:public"],
)
# Native binary for testing on current platform (console only)
go_binary(
name = "eagle_installer",
embed = [":installer_lib"],
@@ -11,10 +11,12 @@ import (
)
var (
fileLocation string
exeLocation string
invitationFile string
ui *UI
fileLocation string
exeLocation string
invitationFile string
ui UIInterface
invitationCode string
replaceInstaller string
)
func init() {
@@ -32,7 +34,7 @@ func main() {
helpFlag := flag.Bool("help", false, "Show help message")
hFlag := flag.Bool("h", false, "Show help message")
codeFlag := flag.String("code", "", "Invitation code for new account registration")
replaceInstaller := flag.String("replace-installer", "", "Path to old installer to replace (internal use)")
replaceFlag := flag.String("replace-installer", "", "Path to old installer to replace (internal use)")
flag.Parse()
if *helpFlag || *hFlag {
@@ -40,21 +42,41 @@ func main() {
return
}
// Initialize UI
ui = NewUI()
// Store flags for use in runInstallerWithUI
invitationCode = *codeFlag
replaceInstaller = *replaceFlag
// Initialize UI - try WebView first, fall back to console
if webviewUI := NewWebViewUI(); webviewUI != nil {
ui = webviewUI
// Run installer logic in a goroutine since WebView needs the main thread
go runInstallerWithUI()
// WebView event loop (blocks until window closes)
ui.Run()
return
}
// Fall back to console UI
ui = NewConsoleUI()
runInstallerWithUI()
}
// runInstallerWithUI runs the installer logic with the initialized UI
func runInstallerWithUI() {
defer ui.Close()
// Clean up temp installer files from previous updates
cleanupTempInstallerFiles()
// Handle installer replacement if launched with --replace-installer flag
if *replaceInstaller != "" {
handleInstallerReplacement(*replaceInstaller)
if replaceInstaller != "" {
handleInstallerReplacement(replaceInstaller)
return
}
// Run the main installer logic
ui.ShowBanner()
if err := runInstallerLogic(*codeFlag); err != nil {
if err := runInstallerLogic(invitationCode); err != nil {
ui.ShowError(fmt.Sprintf("%v", err))
ui.WaitForKeypress("Press Enter to exit...")
os.Exit(1)
@@ -6,21 +6,25 @@ import (
"time"
)
// UI provides a simple console-based progress display
type UI struct {
// ConsoleUI provides a simple console-based progress display.
// It implements UIInterface and serves as the fallback UI when WebView is not available.
type ConsoleUI struct {
lastStatus string
lastProgress int
startTime time.Time
}
func NewUI() *UI {
return &UI{
// Ensure ConsoleUI implements UIInterface
var _ UIInterface = (*ConsoleUI)(nil)
func NewConsoleUI() *ConsoleUI {
return &ConsoleUI{
startTime: time.Now(),
}
}
// ShowBanner displays the application header
func (ui *UI) ShowBanner() {
func (ui *ConsoleUI) ShowBanner() {
fmt.Println()
fmt.Println("╔══════════════════════════════════════════════════════════╗")
fmt.Println("║ Eagle0 Installer ║")
@@ -29,7 +33,7 @@ func (ui *UI) ShowBanner() {
}
// UpdateStatus shows the current status message
func (ui *UI) UpdateStatus(status string) {
func (ui *ConsoleUI) UpdateStatus(status string) {
if status != ui.lastStatus {
ui.lastStatus = status
fmt.Printf("► %s\n", status)
@@ -37,7 +41,7 @@ func (ui *UI) UpdateStatus(status string) {
}
// UpdateProgress shows a progress bar (0-100)
func (ui *UI) UpdateProgress(percent int, description string) {
func (ui *ConsoleUI) UpdateProgress(percent int, description string) {
if percent < 0 {
percent = 0
}
@@ -64,33 +68,43 @@ func (ui *UI) UpdateProgress(percent int, description string) {
}
// ShowSuccess displays a success message
func (ui *UI) ShowSuccess(message string) {
func (ui *ConsoleUI) ShowSuccess(message string) {
fmt.Printf("\n✓ %s\n", message)
}
// ShowError displays an error message
func (ui *UI) ShowError(message string) {
func (ui *ConsoleUI) ShowError(message string) {
fmt.Printf("\n✗ ERROR: %s\n", message)
}
// ShowWarning displays a warning message
func (ui *UI) ShowWarning(message string) {
func (ui *ConsoleUI) ShowWarning(message string) {
fmt.Printf("\n⚠ WARNING: %s\n", message)
}
// ShowInfo displays an info message
func (ui *UI) ShowInfo(message string) {
func (ui *ConsoleUI) ShowInfo(message string) {
fmt.Printf(" %s\n", message)
}
// WaitForKeypress waits for user input before continuing
func (ui *UI) WaitForKeypress(prompt string) {
func (ui *ConsoleUI) WaitForKeypress(prompt string) {
fmt.Printf("\n%s", prompt)
fmt.Scanln()
}
// ShowElapsedTime shows how long the operation took
func (ui *UI) ShowElapsedTime() {
func (ui *ConsoleUI) ShowElapsedTime() {
elapsed := time.Since(ui.startTime)
fmt.Printf("\nCompleted in %.1f seconds\n", elapsed.Seconds())
}
// Run is a no-op for console UI (no event loop needed)
func (ui *ConsoleUI) Run() {
// Console UI doesn't need an event loop
}
// Close is a no-op for console UI (no cleanup needed)
func (ui *ConsoleUI) Close() {
// Console UI doesn't need cleanup
}
@@ -0,0 +1,38 @@
package main
// UIInterface defines the interface for installer UI implementations.
// This allows switching between console UI (fallback) and WebView UI (Windows with WebView2).
type UIInterface interface {
// ShowBanner displays the application header/title
ShowBanner()
// UpdateStatus shows the current status message
UpdateStatus(status string)
// UpdateProgress shows a progress bar (0-100) with description
UpdateProgress(percent int, description string)
// ShowSuccess displays a success message
ShowSuccess(message string)
// ShowError displays an error message
ShowError(message string)
// ShowWarning displays a warning message
ShowWarning(message string)
// ShowInfo displays an info message
ShowInfo(message string)
// WaitForKeypress waits for user input before continuing
WaitForKeypress(prompt string)
// ShowElapsedTime shows how long the operation took
ShowElapsedTime()
// Run starts the UI event loop (blocking for WebView, no-op for console)
Run()
// Close cleans up UI resources
Close()
}
@@ -0,0 +1,31 @@
//go:build !cgo || !windows
package main
// WebViewUI stub for pure Go builds (no CGO).
// This stub allows the code to compile without CGO/WebView dependencies.
// NewWebViewUI returns nil, causing main.go to fall back to ConsoleUI.
type WebViewUI struct{}
// Ensure WebViewUI implements UIInterface (for compile-time checking)
var _ UIInterface = (*WebViewUI)(nil)
// NewWebViewUI returns nil in pure Go builds (no WebView available).
func NewWebViewUI() *WebViewUI {
return nil
}
// The following methods implement UIInterface but will never be called
// since NewWebViewUI returns nil and main.go checks for nil.
func (ui *WebViewUI) ShowBanner() {}
func (ui *WebViewUI) UpdateStatus(status string) {}
func (ui *WebViewUI) UpdateProgress(percent int, desc string) {}
func (ui *WebViewUI) ShowSuccess(message string) {}
func (ui *WebViewUI) ShowError(message string) {}
func (ui *WebViewUI) ShowWarning(message string) {}
func (ui *WebViewUI) ShowInfo(message string) {}
func (ui *WebViewUI) WaitForKeypress(prompt string) {}
func (ui *WebViewUI) ShowElapsedTime() {}
func (ui *WebViewUI) Run() {}
func (ui *WebViewUI) Close() {}
@@ -0,0 +1,268 @@
//go:build cgo && windows
package main
import (
"fmt"
"sync"
"time"
"github.com/webview/webview_go"
)
// WebViewUI provides a WebView2-based GUI for the installer on Windows.
// It implements UIInterface and provides a modern HTML/CSS/JS interface.
type WebViewUI struct {
w webview.WebView
startTime time.Time
ready chan struct{}
mu sync.Mutex
lastStatus string
}
// Ensure WebViewUI implements UIInterface
var _ UIInterface = (*WebViewUI)(nil)
const htmlTemplate = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
color: #e8e8e8;
height: 100vh;
padding: 40px;
display: flex;
flex-direction: column;
}
.logo {
font-size: 42px;
font-weight: 700;
letter-spacing: 4px;
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 30px;
}
.status {
font-size: 16px;
margin: 20px 0;
min-height: 24px;
color: #b8b8b8;
}
.progress-container {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
height: 28px;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
}
.progress-bar {
background: linear-gradient(90deg, #00d4ff 0%, #7b2cbf 100%);
height: 100%;
width: 0%;
transition: width 0.3s ease-out;
border-radius: 12px;
}
.percentage {
text-align: center;
margin-top: 12px;
font-size: 18px;
font-weight: 600;
color: #00d4ff;
}
.message {
margin-top: 20px;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
display: none;
}
.message.success {
display: block;
background: rgba(0, 255, 136, 0.15);
border-left: 4px solid #00ff88;
color: #00ff88;
}
.message.error {
display: block;
background: rgba(255, 71, 87, 0.15);
border-left: 4px solid #ff4757;
color: #ff4757;
}
.message.warning {
display: block;
background: rgba(255, 193, 7, 0.15);
border-left: 4px solid #ffc107;
color: #ffc107;
}
.message.info {
display: block;
background: rgba(0, 212, 255, 0.1);
border-left: 4px solid #00d4ff;
color: #a0d4e8;
}
.elapsed {
margin-top: auto;
font-size: 13px;
color: #666;
}
</style>
</head>
<body>
<div class="logo">EAGLE0</div>
<div class="status" id="status">Initializing...</div>
<div class="progress-container">
<div class="progress-bar" id="progress"></div>
</div>
<div class="percentage" id="percentage">0%</div>
<div class="message" id="message"></div>
<div class="elapsed" id="elapsed"></div>
<script>
function updateProgress(percent, description) {
document.getElementById('progress').style.width = percent + '%';
document.getElementById('percentage').textContent = percent + '%';
if (description) {
document.getElementById('status').textContent = description;
}
}
function updateStatus(status) {
document.getElementById('status').textContent = status;
}
function showMessage(type, text) {
const msg = document.getElementById('message');
msg.className = 'message ' + type;
msg.textContent = text;
}
function showElapsed(seconds) {
document.getElementById('elapsed').textContent = 'Completed in ' + seconds.toFixed(1) + ' seconds';
}
function clearMessage() {
document.getElementById('message').className = 'message';
}
</script>
</body>
</html>`
// NewWebViewUI creates a new WebView-based UI.
// Returns nil if WebView2 is not available.
func NewWebViewUI() *WebViewUI {
w := webview.New(false)
if w == nil {
return nil
}
w.SetTitle("Eagle0 Installer")
w.SetSize(520, 340, webview.HintFixed)
w.SetHtml(htmlTemplate)
ui := &WebViewUI{
w: w,
startTime: time.Now(),
ready: make(chan struct{}),
}
return ui
}
// ShowBanner is a no-op for WebView (logo is in HTML)
func (ui *WebViewUI) ShowBanner() {
// Logo is already in the HTML template
}
// UpdateStatus shows the current status message
func (ui *WebViewUI) UpdateStatus(status string) {
ui.mu.Lock()
defer ui.mu.Unlock()
if status == ui.lastStatus {
return
}
ui.lastStatus = status
ui.w.Dispatch(func() {
ui.w.Eval(fmt.Sprintf("updateStatus(%q)", escapeJS(status)))
})
}
// UpdateProgress shows a progress bar (0-100)
func (ui *WebViewUI) UpdateProgress(percent int, description string) {
if percent < 0 {
percent = 0
}
if percent > 100 {
percent = 100
}
ui.w.Dispatch(func() {
ui.w.Eval(fmt.Sprintf("updateProgress(%d, %q)", percent, escapeJS(description)))
})
}
// ShowSuccess displays a success message
func (ui *WebViewUI) ShowSuccess(message string) {
ui.w.Dispatch(func() {
ui.w.Eval(fmt.Sprintf("showMessage('success', %q)", escapeJS(message)))
})
}
// ShowError displays an error message
func (ui *WebViewUI) ShowError(message string) {
ui.w.Dispatch(func() {
ui.w.Eval(fmt.Sprintf("showMessage('error', %q)", escapeJS(message)))
})
}
// ShowWarning displays a warning message
func (ui *WebViewUI) ShowWarning(message string) {
ui.w.Dispatch(func() {
ui.w.Eval(fmt.Sprintf("showMessage('warning', %q)", escapeJS(message)))
})
}
// ShowInfo displays an info message
func (ui *WebViewUI) ShowInfo(message string) {
ui.w.Dispatch(func() {
ui.w.Eval(fmt.Sprintf("showMessage('info', %q)", escapeJS(message)))
})
}
// WaitForKeypress waits for user acknowledgment
// For WebView, we just pause briefly to let the user see the message
func (ui *WebViewUI) WaitForKeypress(prompt string) {
ui.UpdateStatus(prompt)
time.Sleep(3 * time.Second)
}
// ShowElapsedTime shows how long the operation took
func (ui *WebViewUI) ShowElapsedTime() {
elapsed := time.Since(ui.startTime).Seconds()
ui.w.Dispatch(func() {
ui.w.Eval(fmt.Sprintf("showElapsed(%f)", elapsed))
})
}
// Run starts the WebView event loop (blocking)
func (ui *WebViewUI) Run() {
ui.w.Run()
}
// Close cleans up WebView resources
func (ui *WebViewUI) Close() {
ui.w.Destroy()
}
// escapeJS escapes a string for safe use in JavaScript
func escapeJS(s string) string {
// The %q format in Sprintf handles most escaping, but we use it for the whole string
// So we just return the string as-is since Sprintf("%q", ...) will handle it
return s
}
@@ -32,10 +32,10 @@ type InstallerUpdateInfo struct {
type Updater struct {
serverURL string
httpClient *http.Client
ui *UI
ui UIInterface
}
func NewUpdater(serverURL string, ui *UI) *Updater {
func NewUpdater(serverURL string, ui UIInterface) *Updater {
return &Updater{
serverURL: serverURL,
httpClient: &http.Client{