Switch remote deploy to vendored source builds

Move remote deployment to a vendored source bundle built on the target host via Docker so redeploys no longer require local cross-compilation or host Go installation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
GitHub Actions
2026-05-08 12:19:18 +08:00
parent bb27566e38
commit c1a0fe2949
1320 changed files with 497125 additions and 11 deletions

View File

@@ -0,0 +1,16 @@
ISC License (ISC)
Copyright (c) 2020 John Chadwick
Copyright (c) 2022 Wails Project Developers
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

View File

@@ -0,0 +1,19 @@
# Webviewloader
Webviewloader is a port of [OpenWebView2Loader](https://github.com/jchv/OpenWebView2Loader) to Go.
It is intended to be feature-complete with the original WebView2Loader distributed with
the WebView2 NuGet package, but some features are intentionally not implemented.
## Status
- [x] CompareBrowserVersions
- [x] CreateCoreWebView2Environment
- [x] CreateCoreWebView2EnvironmentWithOptions
- [x] GetAvailableCoreWebView2BrowserVersionString
## Not implemented features
- Registry Overrides of Parameters
- Env Variable Overrides of Parameters
- Does not incorporate `GetCurrentPackageInfo` to search for an installed runtime

View File

@@ -0,0 +1,176 @@
//go:build windows && !native_webview2loader
package webviewloader
import (
"fmt"
"os"
"path/filepath"
"syscall"
"unsafe"
"github.com/wailsapp/go-webview2/pkg/combridge"
"golang.org/x/sys/windows"
)
func init() {
UsingGoWebview2Loader = true
preventEnvAndRegistryOverrides()
}
type webView2RunTimeType int32
const (
webView2RunTimeTypeInstalled webView2RunTimeType = 0x00
webView2RunTimeTypeRedistributable webView2RunTimeType = 0x01
)
// CreateCoreWebView2Environment creates an evergreen WebView2 Environment using the installed WebView2 Runtime version.
//
// This is equivalent to running CreateCoreWebView2EnvironmentWithOptions without any options.
// For more information, see CreateCoreWebView2EnvironmentWithOptions.
func CreateCoreWebView2Environment(environmentCompletedHandler ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler) error {
return CreateCoreWebView2EnvironmentWithOptions(environmentCompletedHandler)
}
// CreateCoreWebView2EnvironmentWithOptions creates an environment with a custom version of WebView2 Runtime,
// user data folder, and with or without additional options.
//
// See https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/webview2-idl?#createcorewebview2environmentwithoptions
func CreateCoreWebView2EnvironmentWithOptions(environmentCompletedHandler ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, opts ...option) error {
var params environmentOptions
for _, opt := range opts {
opt(&params)
}
var err error
var dllPath string
var runtimeType webView2RunTimeType
if browserExecutableFolder := params.browserExecutableFolder; browserExecutableFolder != "" {
runtimeType = webView2RunTimeTypeRedistributable
dllPath, err = findEmbeddedClientDll(browserExecutableFolder)
} else {
runtimeType = webView2RunTimeTypeInstalled
dllPath, _, err = findInstalledClientDll(params.preferCanary)
}
if err != nil {
return err
}
return createWebViewEnvironmentWithClientDll(dllPath, runtimeType, params.userDataFolder,
&params, environmentCompletedHandler)
}
func createWebViewEnvironmentWithClientDll(lpLibFileName string, runtimeType webView2RunTimeType, userDataFolder string,
envOptions *environmentOptions, envCompletedHandler ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler) error {
if !filepath.IsAbs(lpLibFileName) {
return fmt.Errorf("lpLibFileName must be absolute")
}
dll, err := windows.LoadDLL(lpLibFileName)
if err != nil {
return fmt.Errorf("Loading DLL failed: %w", err)
}
defer func() {
canUnloadProc, err := dll.FindProc("DllCanUnloadNow")
if err != nil {
return
}
if r1, _, _ := canUnloadProc.Call(); r1 != windows.NO_ERROR {
return
}
dll.Release()
}()
createProc, err := dll.FindProc("CreateWebViewEnvironmentWithOptionsInternal")
if err != nil {
return fmt.Errorf("Unable to find CreateWebViewEnvironmentWithOptionsInternal entrypoint: %w", err)
}
userDataPtr, err := windows.UTF16PtrFromString(userDataFolder)
if err != nil {
return err
}
envOptionsCom := combridge.New2[iCoreWebView2EnvironmentOptions, iCoreWebView2EnvironmentOptions2](
envOptions, envOptions)
defer envOptionsCom.Close()
envCompletedHandler = &environmentCreatedHandler{envCompletedHandler}
envCompletedCom := combridge.New[iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler](envCompletedHandler)
defer envCompletedCom.Close()
preventEnvAndRegistryOverrides()
const unknown = 1
hr, _, err := createProc.Call(
uintptr(unknown),
uintptr(runtimeType),
uintptr(unsafe.Pointer(userDataPtr)),
uintptr(envOptionsCom.Ref()),
uintptr(envCompletedCom.Ref()))
if hr != 0 {
if err == nil || err == windows.ERROR_SUCCESS {
err = syscall.Errno(hr)
}
return err
}
return nil
}
type environmentCreatedHandler struct {
originalHandler ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler
}
func (r *environmentCreatedHandler) EnvironmentCompleted(errorCode HRESULT, createdEnvironment *ICoreWebView2Environment) HRESULT {
// The OpenWebview2Loader has some retry logic and retries once, didn't encounter any case when this would have been
// needed during the development: https://github.com/jchv/OpenWebView2Loader/blob/master/Source/WebView2Loader.cpp#L202
if createdEnvironment != nil {
// May or may not be necessary, but the official WebView2Loader seems to do it.
iidICoreWebView2Environment := windows.GUID{
Data1: 0xb96d755e,
Data2: 0x0319,
Data3: 0x4e92,
Data4: [8]byte{0xa2, 0x96, 0x23, 0x43, 0x6f, 0x46, 0xa1, 0xfc},
}
if err := createdEnvironment.QueryInterface(&iidICoreWebView2Environment, &createdEnvironment); err != nil {
createdEnvironment = nil
errNo, ok := err.(syscall.Errno)
if !ok {
errNo = syscall.Errno(windows.E_FAIL)
}
errorCode = HRESULT(errNo)
}
}
r.originalHandler.EnvironmentCompleted(errorCode, createdEnvironment)
if createdEnvironment != nil {
createdEnvironment.Release()
}
return HRESULT(windows.S_OK)
}
func preventEnvAndRegistryOverrides() {
// Setting these env variables to empty string also prevents registry overrides because webview2
// checks for existence and not for empty value
os.Setenv("WEBVIEW2_PIPE_FOR_SCRIPT_DEBUGGER", "")
os.Setenv("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "")
os.Setenv("WEBVIEW2_RELEASE_CHANNEL_PREFERENCE", "0")
// The following seems not be be required because those are only used by the webview2loader which
// in this case is implemented on our own. But nevertheless set them to empty to be consistent.
os.Setenv("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", "")
os.Setenv("WEBVIEW2_USER_DATA_FOLDER", "")
}

View File

@@ -0,0 +1,42 @@
//go:build windows && !native_webview2loader
package webviewloader
import (
"github.com/wailsapp/go-webview2/pkg/combridge"
)
// HRESULT
//
// See https://docs.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values
type HRESULT int32
// ICoreWebView2Environment Represents the WebView2 Environment
//
// See https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2environment
type ICoreWebView2Environment = combridge.IUnknownImpl
// ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler receives the WebView2Environment created using CreateCoreWebView2Environment.
type ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler interface {
// EnvironmentCompleted is invoked to receive the created WebView2Environment
//
// See https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2createcorewebview2environmentcompletedhandler?#invoke
EnvironmentCompleted(errorCode HRESULT, createdEnvironment *ICoreWebView2Environment) HRESULT
}
type iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler interface {
combridge.IUnknown
ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler
}
func init() {
combridge.RegisterVTable[combridge.IUnknown, iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler](
"{4e8a3389-c9d8-4bd2-b6b5-124fee6cc14d}",
_iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandlerInvoke,
)
}
func _iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandlerInvoke(this uintptr, errorCode HRESULT, env *combridge.IUnknownImpl) uintptr {
res := combridge.Resolve[iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler](this).EnvironmentCompleted(errorCode, env)
return uintptr(res)
}

View File

@@ -0,0 +1,276 @@
//go:build windows && !native_webview2loader
package webviewloader
import (
"unicode/utf16"
"unsafe"
"github.com/wailsapp/go-webview2/pkg/combridge"
"golang.org/x/sys/windows"
)
// WithBrowserExecutableFolder to specify whether WebView2 controls use a fixed or installed version
// of the WebView2 Runtime that exists on a user machine.
//
// To use a fixed version of the WebView2 Runtime,
// pass the folder path that contains the fixed version of the WebView2 Runtime.
// BrowserExecutableFolder supports both relative (to the application's executable) and absolute files paths.
// To create WebView2 controls that use the installed version of the WebView2 Runtime that exists on user
// machines, pass a empty string to WithBrowserExecutableFolder. In this scenario, the API tries to find a
// compatible version of the WebView2 Runtime that is installed on the user machine (first at the machine level,
// and then per user) using the selected channel preference. The path of fixed version of the WebView2 Runtime
// should not contain \Edge\Application\. When such a path is used, the API fails with HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED).
func WithBrowserExecutableFolder(folder string) option {
return func(wvep *environmentOptions) {
wvep.browserExecutableFolder = folder
}
}
// WithUserDataFolder specifies to user data folder location for WebView2
//
// You may specify the userDataFolder to change the default user data folder location for WebView2.
// The path is either an absolute file path or a relative file path that is interpreted as relative
// to the compiled code for the current process.
// Dhe default user data ({Executable File Name}.WebView2) folder is created in the same directory
// next to the compiled code for the app. WebView2 creation fails if the compiled code is running
// in a directory in which the process does not have permission to create a new directory.
// The app is responsible to clean up the associated user data folder when it is done.
func WithUserDataFolder(folder string) option {
return func(wvep *environmentOptions) {
wvep.userDataFolder = folder
}
}
// WithAdditionalBrowserArguments changes the behavior of the WebView.
//
// The arguments are passed to the
// browser process as part of the command. For more information about
// using command-line switches with Chromium browser processes, navigate to
// [Run Chromium with Flags][ChromiumDevelopersHowTosRunWithFlags].
// The value appended to a switch is appended to the browser process, for
// example, in `--edge-webview-switches=xxx` the value is `xxx`. If you
// specify a switch that is important to WebView functionality, it is
// ignored, for example, `--user-data-dir`. Specific features are disabled
// internally and blocked from being enabled. If a switch is specified
// multiple times, only the last instance is used.
//
// \> [!NOTE]\n\> A merge of the different values of the same switch is not attempted,
// except for disabled and enabled features. The features specified by
// `--enable-features` and `--disable-features` are merged with simple
// logic.\n\> * The features is the union of the specified features
// and built-in features. If a feature is disabled, it is removed from the
// enabled features list.
//
// If you specify command-line switches and use the
// `additionalBrowserArguments` parameter, the `--edge-webview-switches`
// value takes precedence and is processed last. If a switch fails to
// parse, the switch is ignored. The default state for the operation is
// to run the browser process with no extra flags.
//
// [ChromiumDevelopersHowTosRunWithFlags]: https://www.chromium.org/developers/how-tos/run-chromium-with-flags "Run Chromium with flags | The Chromium Projects"
func WithAdditionalBrowserArguments(args string) option {
return func(wvep *environmentOptions) {
wvep.additionalBrowserArguments = args
}
}
// WithLanguage sets the default display language for WebView.
//
// It applies to browser UI such as
// context menu and dialogs. It also applies to the `accept-languages` HTTP
// header that WebView sends to websites. It is in the format of
//
// `language[-country]` where `language` is the 2-letter code from
// [ISO 639][ISO639LanguageCodesHtml]
// and `country` is the
// 2-letter code from
// [ISO 3166][ISOStandard72482Html].
//
// [ISO639LanguageCodesHtml]: https://www.iso.org/iso-639-language-codes.html "ISO 639 | ISO"
// [ISOStandard72482Html]: https://www.iso.org/standard/72482.html "ISO 3166-1:2020 | ISO"
func WithLanguage(lang string) option {
return func(wvep *environmentOptions) {
wvep.language = lang
}
}
// WithTargetCompatibleBrowserVersion secifies the version of the WebView2 Runtime binaries required to be
// compatible with your app.
//
// This defaults to the WebView2 Runtime version
// that corresponds with the version of the SDK the app is using. The
// format of this value is the same as the format of the
// `BrowserVersionString` property and other `BrowserVersion` values. Only
// the version part of the `BrowserVersion` value is respected. The channel
// suffix, if it exists, is ignored. The version of the WebView2 Runtime
// binaries actually used may be different from the specified
// `TargetCompatibleBrowserVersion`. The binaries are only guaranteed to be
// compatible. Verify the actual version on the `BrowserVersionString`
// property on the `ICoreWebView2Environment`.
func WithTargetCompatibleBrowserVersion(version string) option {
return func(wvep *environmentOptions) {
wvep.targetCompatibleBrowserVersion = version
}
}
// WithAllowSingleSignOnUsingOSPrimaryAccount is used to enable
// single sign on with Azure Active Directory (AAD) and personal Microsoft
// Account (MSA) resources inside WebView. All AAD accounts, connected to
// Windows and shared for all apps, are supported. For MSA, SSO is only enabled
// for the account associated for Windows account login, if any.
// Default is disabled. Universal Windows Platform apps must also declare
// `enterpriseCloudSSO`
// [Restricted capabilities][WindowsUwpPackagingAppCapabilityDeclarationsRestrictedCapabilities]
// for the single sign on (SSO) to work.
//
// [WindowsUwpPackagingAppCapabilityDeclarationsRestrictedCapabilities]: /windows/uwp/packaging/app-capability-declarations\#restricted-capabilities "Restricted capabilities - App capability declarations | Microsoft Docs"
func WithAllowSingleSignOnUsingOSPrimaryAccount(allow bool) option {
return func(wvep *environmentOptions) {
wvep.allowSingleSignOnUsingOSPrimaryAccount = allow
}
}
// WithExclusiveUserDataFolderAccess specifies that the WebView environment
// obtains exclusive access to the user data folder.
//
// If the user data folder is already being used by another WebView environment with a
// different value for `ExclusiveUserDataFolderAccess` property, the creation of a WebView2Controller
// using the environment object will fail with `HRESULT_FROM_WIN32(ERROR_INVALID_STATE)`.
// When set as TRUE, no other WebView can be created from other processes using WebView2Environment
// objects with the same UserDataFolder. This prevents other processes from creating WebViews
// which share the same browser process instance, since sharing is performed among
// WebViews that have the same UserDataFolder. When another process tries to create a
// WebView2Controller from an WebView2Environment object created with the same user data folder,
// it will fail with `HRESULT_FROM_WIN32(ERROR_INVALID_STATE)`.
func WithExclusiveUserDataFolderAccess(exclusive bool) option {
return func(wvep *environmentOptions) {
wvep.exclusiveUserDataFolderAccess = exclusive
}
}
type option func(*environmentOptions)
var _ iCoreWebView2EnvironmentOptions = &environmentOptions{}
var _ iCoreWebView2EnvironmentOptions2 = &environmentOptions{}
type environmentOptions struct {
browserExecutableFolder string
userDataFolder string
preferCanary bool
additionalBrowserArguments string
language string
targetCompatibleBrowserVersion string
allowSingleSignOnUsingOSPrimaryAccount bool
exclusiveUserDataFolderAccess bool
}
func (o *environmentOptions) AdditionalBrowserArguments() string {
return o.additionalBrowserArguments
}
func (o *environmentOptions) Language() string {
return o.language
}
func (o *environmentOptions) TargetCompatibleBrowserVersion() string {
v := o.targetCompatibleBrowserVersion
if v == "" {
v = kMinimumCompatibleVersion
}
return v
}
func (o *environmentOptions) AllowSingleSignOnUsingOSPrimaryAccount() bool {
return o.allowSingleSignOnUsingOSPrimaryAccount
}
func (o *environmentOptions) ExclusiveUserDataFolderAccess() bool {
return o.exclusiveUserDataFolderAccess
}
type iCoreWebView2EnvironmentOptions interface {
combridge.IUnknown
AdditionalBrowserArguments() string
Language() string
TargetCompatibleBrowserVersion() string
AllowSingleSignOnUsingOSPrimaryAccount() bool
}
type iCoreWebView2EnvironmentOptions2 interface {
combridge.IUnknown
ExclusiveUserDataFolderAccess() bool
}
func init() {
combridge.RegisterVTable[combridge.IUnknown, iCoreWebView2EnvironmentOptions](
"{2fde08a8-1e9a-4766-8c05-95a9ceb9d1c5}",
_iCoreWebView2EnvironmentOptionsAdditionalBrowserArguments,
_iCoreWebView2EnvironmentOptionsNOP,
_iCoreWebView2EnvironmentOptionsLanguage,
_iCoreWebView2EnvironmentOptionsNOP,
_iCoreWebView2EnvironmentTargetCompatibleBrowserVersion,
_iCoreWebView2EnvironmentOptionsNOP,
_iCoreWebView2EnvironmentOptionsAllowSingleSignOnUsingOSPrimaryAccount,
_iCoreWebView2EnvironmentOptionsNOP,
)
combridge.RegisterVTable[combridge.IUnknown, iCoreWebView2EnvironmentOptions2](
"{ff85c98a-1ba7-4a6b-90c8-2b752c89e9e2}",
_iCoreWebView2EnvironmentOptions2ExclusiveUserDataFolderAccess,
_iCoreWebView2EnvironmentOptionsNOP,
)
}
func _iCoreWebView2EnvironmentOptionsNOP(this uintptr) uintptr {
return uintptr(windows.S_FALSE)
}
func _iCoreWebView2EnvironmentOptionsAdditionalBrowserArguments(this uintptr, value **uint16) uintptr {
v := combridge.Resolve[iCoreWebView2EnvironmentOptions](this).AdditionalBrowserArguments()
*value = stringToOleString(v)
return uintptr(windows.S_OK)
}
func _iCoreWebView2EnvironmentOptionsLanguage(this uintptr, value **uint16) uintptr {
args := combridge.Resolve[iCoreWebView2EnvironmentOptions](this).Language()
*value = stringToOleString(args)
return uintptr(windows.S_OK)
}
func _iCoreWebView2EnvironmentTargetCompatibleBrowserVersion(this uintptr, value **uint16) uintptr {
args := combridge.Resolve[iCoreWebView2EnvironmentOptions](this).TargetCompatibleBrowserVersion()
*value = stringToOleString(args)
return uintptr(windows.S_OK)
}
func _iCoreWebView2EnvironmentOptionsAllowSingleSignOnUsingOSPrimaryAccount(this uintptr, value *int32) uintptr {
v := combridge.Resolve[iCoreWebView2EnvironmentOptions](this).AllowSingleSignOnUsingOSPrimaryAccount()
*value = boolToInt(v)
return uintptr(windows.S_OK)
}
func _iCoreWebView2EnvironmentOptions2ExclusiveUserDataFolderAccess(this uintptr, value *int32) uintptr {
v := combridge.Resolve[iCoreWebView2EnvironmentOptions2](this).ExclusiveUserDataFolderAccess()
*value = boolToInt(v)
return uintptr(windows.S_OK)
}
func stringToOleString(v string) *uint16 {
wstr := utf16.Encode([]rune(v + "\x00"))
lwstr := len(wstr)
ptr := (*uint16)(coTaskMemAlloc(2 * lwstr))
copy(unsafe.Slice(ptr, lwstr), wstr)
return ptr
}
func boolToInt(v bool) int32 {
if v {
return 1
}
return 0
}

View File

@@ -0,0 +1,74 @@
//go:build windows
package webviewloader
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"golang.org/x/sys/windows/registry"
)
var (
errNoClientDLLFound = errors.New("no webview2 found")
)
func findEmbeddedBrowserVersion(filename string) (string, error) {
block, err := getFileVersionInfo(filename)
if err != nil {
return "", err
}
info, err := verQueryValueString(block, "\\StringFileInfo\\040904B0\\ProductVersion")
if err != nil {
return "", err
}
return info, nil
}
func findEmbeddedClientDll(embeddedEdgeSubFolder string) (outClientPath string, err error) {
if !filepath.IsAbs(embeddedEdgeSubFolder) {
exe, err := os.Executable()
if err != nil {
return "", err
}
embeddedEdgeSubFolder = filepath.Join(filepath.Dir(exe), embeddedEdgeSubFolder)
}
return findClientDllInFolder(embeddedEdgeSubFolder)
}
func findClientDllInFolder(folder string) (string, error) {
arch := ""
switch runtime.GOARCH {
case "arm64":
arch = "arm64"
case "amd64":
arch = "x64"
case "386":
arch = "x86"
default:
return "", fmt.Errorf("Unsupported architecture")
}
dllPath := filepath.Join(folder, "EBWebView", arch, "EmbeddedBrowserWebView.dll")
if _, err := os.Stat(dllPath); err != nil {
return "", mapFindErr(err)
}
return dllPath, nil
}
func mapFindErr(err error) error {
if errors.Is(err, registry.ErrNotExist) {
return errNoClientDLLFound
}
if errors.Is(err, os.ErrNotExist) {
return errNoClientDLLFound
}
return err
}

View File

@@ -0,0 +1,94 @@
//go:build windows && !native_webview2loader
package webviewloader
import (
"path/filepath"
"golang.org/x/sys/windows/registry"
)
const (
kNumChannels = 4
kInstallKeyPath = "Software\\Microsoft\\EdgeUpdate\\ClientState\\"
kMinimumCompatibleVersion = "86.0.616.0"
)
var (
kChannelName = [kNumChannels]string{
"", "beta", "dev", "canary", // "internal"
}
kChannelUuid = [kNumChannels]string{
"{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
"{2CD8A007-E189-409D-A2C8-9AF4EF3C72AA}",
"{0D50BFEC-CD6A-4F9A-964C-C7416E3ACB10}",
"{65C35B14-6C1D-4122-AC46-7148CC9D6497}",
//"{BE59E8FD-089A-411B-A3B0-051D9E417818}",
}
minimumCompatibleVersion, _ = parseVersion(kMinimumCompatibleVersion)
)
func findInstalledClientDll(preferCanary bool) (clientPath string, version *version, err error) {
for i := 0; i < kNumChannels; i++ {
channel := i
if preferCanary {
channel = (kNumChannels - 1) - i
}
key := kInstallKeyPath + kChannelUuid[channel]
for _, checkSystem := range []bool{true, false} {
clientPath, version, err := findInstalledClientDllForChannel(key, checkSystem)
if err == errNoClientDLLFound {
continue
}
if err != nil {
return "", nil, err
}
version.channel = kChannelName[channel]
return clientPath, version, nil
}
}
return "", nil, errNoClientDLLFound
}
func findInstalledClientDllForChannel(subKey string, system bool) (clientPath string, clientVersion *version, err error) {
key := registry.LOCAL_MACHINE
if !system {
key = registry.CURRENT_USER
}
regKey, err := registry.OpenKey(key, subKey, registry.READ|registry.WOW64_32KEY)
if err != nil {
return "", nil, mapFindErr(err)
}
defer regKey.Close()
embeddedEdgeSubFolder, _, err := regKey.GetStringValue("EBWebView")
if err != nil {
return "", nil, mapFindErr(err)
}
if embeddedEdgeSubFolder == "" {
return "", nil, errNoClientDLLFound
}
versionString := filepath.Base(embeddedEdgeSubFolder)
version, err := parseVersion(versionString)
if err != nil {
return "", nil, errNoClientDLLFound
}
if version.compare(minimumCompatibleVersion) < 0 {
return "", nil, errNoClientDLLFound
}
dllPath, err := findEmbeddedClientDll(embeddedEdgeSubFolder)
if err != nil {
return "", nil, mapFindErr(err)
}
return dllPath, &version, nil
}

View File

@@ -0,0 +1,173 @@
//go:build windows && native_webview2loader
package webviewloader
import (
"errors"
"fmt"
"os"
"sync"
"unsafe"
"github.com/jchv/go-winloader"
"golang.org/x/sys/windows"
)
func init() {
preventEnvAndRegistryOverrides(nil, nil, "")
}
var (
memOnce sync.Once
memModule winloader.Module
memCreate winloader.Proc
memCompareBrowserVersions winloader.Proc
memGetAvailableCoreWebView2BrowserVersionString winloader.Proc
memErr error
)
const (
// https://referencesource.microsoft.com/#system.web/Util/hresults.cs,20
E_FILENOTFOUND = 0x80070002
)
// CompareBrowserVersions will compare the 2 given versions and return:
//
// Less than zero: v1 < v2
// zero: v1 == v2
// Greater than zero: v1 > v2
func CompareBrowserVersions(v1 string, v2 string) (int, error) {
_v1, err := windows.UTF16PtrFromString(v1)
if err != nil {
return 0, err
}
_v2, err := windows.UTF16PtrFromString(v2)
if err != nil {
return 0, err
}
err = loadFromMemory()
if err != nil {
return 0, err
}
var result int32
_, _, err = memCompareBrowserVersions.Call(
uint64(uintptr(unsafe.Pointer(_v1))),
uint64(uintptr(unsafe.Pointer(_v2))),
uint64(uintptr(unsafe.Pointer(&result))))
if err != windows.ERROR_SUCCESS {
return 0, err
}
return int(result), nil
}
// GetAvailableCoreWebView2BrowserVersionString returns version of the webview2 runtime.
// If path is empty, it will try to find installed webview2 is the system.
// If there is no version installed, a blank string is returned.
func GetAvailableCoreWebView2BrowserVersionString(path string) (string, error) {
if path != "" {
// The default implementation fails if CGO and a fixed browser path is used. It's caused by the go-winloader
// which loads the native DLL from memory.
// Use the new GoWebView2Loader in this case, in the future we will make GoWebView2Loader
// feature-complete and remove the use of the native DLL and go-winloader.
version, err := goGetAvailableCoreWebView2BrowserVersionString(path)
if errors.Is(err, errNoClientDLLFound) {
// WebView2 is not found
return "", nil
} else if err != nil {
return "", err
}
return version, nil
}
err := loadFromMemory()
if err != nil {
return "", err
}
var browserPath *uint16 = nil
if path != "" {
browserPath, err = windows.UTF16PtrFromString(path)
if err != nil {
return "", fmt.Errorf("error calling UTF16PtrFromString for %s: %v", path, err)
}
}
preventEnvAndRegistryOverrides(browserPath, nil, "")
var result *uint16
res, _, err := memGetAvailableCoreWebView2BrowserVersionString.Call(
uint64(uintptr(unsafe.Pointer(browserPath))),
uint64(uintptr(unsafe.Pointer(&result))))
if res != 0 {
if res == E_FILENOTFOUND {
// WebView2 is not installed
return "", nil
}
return "", fmt.Errorf("Unable to call GetAvailableCoreWebView2BrowserVersionString (%x): %w", res, err)
}
version := windows.UTF16PtrToString(result)
windows.CoTaskMemFree(unsafe.Pointer(result))
return version, nil
}
// CreateCoreWebView2EnvironmentWithOptions tries to load WebviewLoader2 and
// call the CreateCoreWebView2EnvironmentWithOptions routine.
func CreateCoreWebView2EnvironmentWithOptions(browserExecutableFolder, userDataFolder *uint16, environmentCompletedHandle uintptr, additionalBrowserArgs string) (uintptr, error) {
err := loadFromMemory()
if err != nil {
return 0, err
}
preventEnvAndRegistryOverrides(browserExecutableFolder, userDataFolder, additionalBrowserArgs)
res, _, _ := memCreate.Call(
uint64(uintptr(unsafe.Pointer(browserExecutableFolder))),
uint64(uintptr(unsafe.Pointer(userDataFolder))),
0,
uint64(environmentCompletedHandle),
)
return uintptr(res), nil
}
func loadFromMemory() error {
var err error
// DLL is not available natively. Try loading embedded copy.
memOnce.Do(func() {
memModule, memErr = winloader.LoadFromMemory(WebView2Loader)
if memErr != nil {
err = fmt.Errorf("Unable to load WebView2Loader.dll from memory: %w", memErr)
return
}
memCreate = memModule.Proc("CreateCoreWebView2EnvironmentWithOptions")
memCompareBrowserVersions = memModule.Proc("CompareBrowserVersions")
memGetAvailableCoreWebView2BrowserVersionString = memModule.Proc("GetAvailableCoreWebView2BrowserVersionString")
})
return err
}
func preventEnvAndRegistryOverrides(browserFolder, userDataFolder *uint16, additionalBrowserArgs string) {
// Setting these env variables to empty string also prevents registry overrides because webview2loader
// checks for existence and not for empty value
os.Setenv("WEBVIEW2_PIPE_FOR_SCRIPT_DEBUGGER", "")
// Set these overrides to the values or empty to prevent registry and external env overrides
os.Setenv("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", additionalBrowserArgs)
os.Setenv("WEBVIEW2_RELEASE_CHANNEL_PREFERENCE", "0")
os.Setenv("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", windows.UTF16PtrToString(browserFolder))
os.Setenv("WEBVIEW2_USER_DATA_FOLDER", windows.UTF16PtrToString(userDataFolder))
}
func goGetAvailableCoreWebView2BrowserVersionString(browserExecutableFolder string) (string, error) {
clientPath, err := findEmbeddedClientDll(browserExecutableFolder)
if err != nil {
return "", err
}
return findEmbeddedBrowserVersion(clientPath)
}

View File

@@ -0,0 +1,8 @@
//go:build windows && native_webview2loader
package webviewloader
import _ "embed"
//go:embed x86/WebView2Loader.dll
var WebView2Loader []byte

View File

@@ -0,0 +1,8 @@
//go:build windows && native_webview2loader
package webviewloader
import _ "embed"
//go:embed x64/WebView2Loader.dll
var WebView2Loader []byte

View File

@@ -0,0 +1,8 @@
//go:build windows && native_webview2loader
package webviewloader
import _ "embed"
//go:embed arm64/WebView2Loader.dll
var WebView2Loader []byte

View File

@@ -0,0 +1,143 @@
//go:build windows
package webviewloader
import (
"fmt"
"syscall"
"unicode/utf16"
"unsafe"
"golang.org/x/sys/windows"
)
var (
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
procGlobalAlloc = modkernel32.NewProc("GlobalAlloc")
procGlobalFree = modkernel32.NewProc("GlobalFree")
modversion = windows.NewLazySystemDLL("version.dll")
procGetFileVersionInfoSize = modversion.NewProc("GetFileVersionInfoSizeW")
procGetFileVersionInfo = modversion.NewProc("GetFileVersionInfoW")
procVerQueryValue = modversion.NewProc("VerQueryValueW")
modole32 = windows.NewLazySystemDLL("ole32.dll")
procCoTaskMemAlloc = modole32.NewProc("CoTaskMemAlloc")
)
func getFileVersionInfo(path string) ([]byte, error) {
lptstrFilename, err := syscall.UTF16PtrFromString(path)
if err != nil {
return nil, err
}
size, _, err := procGetFileVersionInfoSize.Call(
uintptr(unsafe.Pointer(lptstrFilename)),
0,
)
err = maskErrorSuccess(err)
if size == 0 && err == nil {
err = fmt.Errorf("GetFileVersionInfoSize failed")
}
if err != nil {
return nil, err
}
data := make([]byte, size)
ret, _, err := procGetFileVersionInfo.Call(
uintptr(unsafe.Pointer(lptstrFilename)),
0,
uintptr(size),
uintptr(unsafe.Pointer(&data[0])),
)
err = maskErrorSuccess(err)
if ret == 0 && err == nil {
err = fmt.Errorf("GetFileVersionInfo failed")
}
if err != nil {
return nil, err
}
return data, nil
}
func verQueryValueString(block []byte, subBlock string) (string, error) {
// Allocate memory from native side to make sure the block doesn't get moved
// because we get a pointer into that memory block from the native verQueryValue
// call back.
pBlock := globalAlloc(0, uint32(len(block)))
defer globalFree(unsafe.Pointer(pBlock))
// Copy the memory region into native side memory
copy(unsafe.Slice((*byte)(pBlock), len(block)), block)
lpSubBlock, err := syscall.UTF16PtrFromString(subBlock)
if err != nil {
return "", err
}
var lplpBuffer unsafe.Pointer
var puLen uint
ret, _, err := procVerQueryValue.Call(
uintptr(pBlock),
uintptr(unsafe.Pointer(lpSubBlock)),
uintptr(unsafe.Pointer(&lplpBuffer)),
uintptr(unsafe.Pointer(&puLen)),
)
err = maskErrorSuccess(err)
if ret == 0 && err == nil {
err = fmt.Errorf("VerQueryValue failed")
}
if err != nil {
return "", err
}
if puLen <= 1 {
return "", nil
}
puLen -= 1 // Remove Null-Terminator
wchar := unsafe.Slice((*uint16)(lplpBuffer), puLen)
return string(utf16.Decode(wchar)), nil
}
func globalAlloc(uFlags uint, dwBytes uint32) unsafe.Pointer {
ret, _, _ := procGlobalAlloc.Call(
uintptr(uFlags),
uintptr(dwBytes))
if ret == 0 {
panic("globalAlloc failed")
}
return unsafe.Pointer(ret)
}
func globalFree(data unsafe.Pointer) {
ret, _, _ := procGlobalFree.Call(uintptr(data))
if ret != 0 {
panic("globalFree failed")
}
}
func maskErrorSuccess(err error) error {
if err == windows.ERROR_SUCCESS {
return nil
}
return err
}
func coTaskMemAlloc(size int) unsafe.Pointer {
ret, _, _ := procCoTaskMemAlloc.Call(
uintptr(size))
if ret == 0 {
panic("coTaskMemAlloc failed")
}
return unsafe.Pointer(ret)
}

View File

@@ -0,0 +1,150 @@
//go:build windows && !native_webview2loader
package webviewloader
import (
"errors"
"fmt"
"strconv"
"strings"
)
// UsingGoWebview2Loader is set to true when the go webview2loader is used.
var UsingGoWebview2Loader bool
// CompareBrowserVersions will compare the 2 given versions and return:
//
// -1 = v1 < v2
// 0 = v1 == v2
// 1 = v1 > v2
func CompareBrowserVersions(v1 string, v2 string) (int, error) {
v, err := parseVersion(v1)
if err != nil {
return 0, fmt.Errorf("v1 invalid: %w", err)
}
w, err := parseVersion(v2)
if err != nil {
return 0, fmt.Errorf("v2 invalid: %w", err)
}
return v.compare(w), nil
}
// GetAvailableCoreWebView2BrowserVersionString get the browser version info including channel name
// if it is the WebView2 Runtime.
// Channel names are Beta, Dev, and Canary.
func GetAvailableCoreWebView2BrowserVersionString(browserExecutableFolder string) (string, error) {
if browserExecutableFolder != "" {
clientPath, err := findEmbeddedClientDll(browserExecutableFolder)
if errors.Is(err, errNoClientDLLFound) {
// WebView2 is not found
return "", nil
} else if err != nil {
return "", err
}
return findEmbeddedBrowserVersion(clientPath)
}
_, version, err := findInstalledClientDll(false)
if errors.Is(err, errNoClientDLLFound) {
return "", nil
} else if err != nil {
return "", err
}
return version.String(), nil
}
type version struct {
major int
minor int
patch int
build int
channel string
}
func (v version) String() string {
vv := fmt.Sprintf("%d.%d.%d.%d", v.major, v.minor, v.patch, v.build)
if v.channel != "" {
vv += " " + v.channel
}
return vv
}
func (v version) compare(o version) int {
if c := compareInt(v.major, o.major); c != 0 {
return c
}
if c := compareInt(v.minor, o.minor); c != 0 {
return c
}
if c := compareInt(v.patch, o.patch); c != 0 {
return c
}
return compareInt(v.build, o.build)
}
func parseVersion(v string) (version, error) {
var p version
// Split away channel information...
if i := strings.Index(v, " "); i > 0 {
p.channel = v[i+1:]
v = v[:i]
}
vv := strings.Split(v, ".")
if len(vv) > 4 {
return p, fmt.Errorf("too many version parts")
}
var err error
vv, p.major, err = parseInt(vv)
if err != nil {
return p, fmt.Errorf("bad major version: %w", err)
}
vv, p.minor, err = parseInt(vv)
if err != nil {
return p, fmt.Errorf("bad minor version: %w", err)
}
vv, p.patch, err = parseInt(vv)
if err != nil {
return p, fmt.Errorf("bad patch version: %w", err)
}
_, p.build, err = parseInt(vv)
if err != nil {
return p, fmt.Errorf("bad build version: %w", err)
}
return p, nil
}
func parseInt(v []string) ([]string, int, error) {
if len(v) == 0 {
return nil, 0, nil
}
p, err := strconv.ParseInt(v[0], 10, 32)
if err != nil {
return nil, 0, err
}
return v[1:], int(p), nil
}
func compareInt(v1, v2 int) int {
if v1 == v2 {
return 0
}
if v1 < v2 {
return -1
} else {
return +1
}
}