feat: update Wails to alpha 74, add iOS/Android/Docker build support
Some checks failed
Security Scan / security (push) Successful in 8s
Test / test (push) Failing after 1m41s

- Updated wails3 CLI to v3.0.0-alpha.74
- Added build/ios/, build/android/, build/docker/ from new template
- Updated all platform Taskfiles (darwin, linux, windows)
- Fixed Angular versions to ~20.3.16
- Root Taskfile with platform-aware build/package/run
- Binary compiles and runs — system tray appears on macOS

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-14 13:10:55 +00:00
parent d828c6356a
commit bfd5c59cac
60 changed files with 4351 additions and 1056 deletions

2
.gitignore vendored
View file

@ -4,4 +4,4 @@ bin
frontend/dist frontend/dist
frontend/node_modules frontend/node_modules
build/linux/appimage/build build/linux/appimage/build
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe build/windows/nsis/MicrosoftEdgeWebview2Setup.exeide

33
Taskfile.yml Normal file
View file

@ -0,0 +1,33 @@
version: '3'
includes:
common: ./build/Taskfile.yml
windows: ./build/windows/Taskfile.yml
darwin: ./build/darwin/Taskfile.yml
linux: ./build/linux/Taskfile.yml
vars:
APP_NAME: "core-ide"
BIN_DIR: "bin"
VITE_PORT: '{{.WAILS_VITE_PORT | default 9247}}'
tasks:
build:
summary: Builds the application
cmds:
- task: "{{OS}}:build"
package:
summary: Packages a production build of the application
cmds:
- task: "{{OS}}:package"
run:
summary: Runs the application
cmds:
- task: "{{OS}}:run"
dev:
summary: Runs the application in development mode
cmds:
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}

View file

@ -14,19 +14,20 @@ tasks:
- package.json - package.json
- package-lock.json - package-lock.json
generates: generates:
- node_modules/* - node_modules
preconditions: preconditions:
- sh: npm version - sh: npm version
msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/" msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
cmds: cmds:
- npm install - npm install --legacy-peer-deps
build:frontend: build:frontend:
label: build:frontend (PRODUCTION={{.PRODUCTION}}) label: build:frontend (DEV={{.DEV}})
summary: Build the frontend project summary: Build the frontend project
dir: frontend dir: frontend
sources: sources:
- "**/*" - "**/*"
- exclude: node_modules/**/*
generates: generates:
- dist/**/* - dist/**/*
deps: deps:
@ -38,10 +39,9 @@ tasks:
cmds: cmds:
- npm run {{.BUILD_COMMAND}} -q - npm run {{.BUILD_COMMAND}} -q
env: env:
PRODUCTION: '{{.PRODUCTION | default "false"}}' PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}'
vars: vars:
BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build:dev{{end}}' BUILD_COMMAND: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}'
generate:bindings: generate:bindings:
label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}})
@ -51,25 +51,26 @@ tasks:
sources: sources:
- "**/*.[jt]s" - "**/*.[jt]s"
- exclude: frontend/**/* - exclude: frontend/**/*
- frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output - frontend/bindings/**/*
- "**/*.go" - "**/*.go"
- go.mod - go.mod
- go.sum - go.sum
generates: generates:
- frontend/bindings/**/* - frontend/bindings/**/*
cmds: cmds:
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=false -ts -i - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true -ts
generate:icons: generate:icons:
summary: Generates Windows `.ico` and Mac `.icns` files from an image summary: Generates Windows `.ico` and Mac `.icns` files from an image
dir: build dir: build
sources: sources:
- "appicon.png" - "appicon.png"
- "appicon.icon"
generates: generates:
- "darwin/icons.icns" - "darwin/icons.icns"
- "windows/icon.ico" - "windows/icon.ico"
cmds: cmds:
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin
dev:frontend: dev:frontend:
summary: Runs the frontend in development mode summary: Runs the frontend in development mode
@ -77,9 +78,7 @@ tasks:
deps: deps:
- task: install:frontend:deps - task: install:frontend:deps
cmds: cmds:
- npm run dev -- --port {{.VITE_PORT}} - npm run dev -- --port {{.VITE_PORT}} --strictPort
vars:
VITE_PORT: '{{.VITE_PORT | default "5173"}}'
update:build-assets: update:build-assets:
summary: Updates the build assets summary: Updates the build assets

237
build/android/Taskfile.yml Normal file
View file

@ -0,0 +1,237 @@
version: '3'
includes:
common: ../Taskfile.yml
vars:
APP_ID: '{{.APP_ID | default "com.wails.app"}}'
MIN_SDK: '21'
TARGET_SDK: '34'
NDK_VERSION: 'r26d'
tasks:
install:deps:
summary: Check and install Android development dependencies
cmds:
- go run build/android/scripts/deps/install_deps.go
env:
TASK_FORCE_YES: '{{if .YES}}true{{else}}false{{end}}'
prompt: This will check and install Android development dependencies. Continue?
build:
summary: Creates a build of the application for Android
deps:
- task: common:go:mod:tidy
- task: generate:android:bindings
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
PRODUCTION:
ref: .PRODUCTION
- task: common:generate:icons
cmds:
- echo "Building Android app {{.APP_NAME}}..."
- task: compile:go:shared
vars:
ARCH: '{{.ARCH | default "arm64"}}'
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}'
env:
PRODUCTION: '{{.PRODUCTION | default "false"}}'
compile:go:shared:
summary: Compile Go code to shared library (.so)
cmds:
- |
NDK_ROOT="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/{{.NDK_VERSION}}}"
if [ ! -d "$NDK_ROOT" ]; then
echo "Error: Android NDK not found at $NDK_ROOT"
echo "Please set ANDROID_NDK_HOME or install NDK {{.NDK_VERSION}} via Android Studio"
exit 1
fi
# Determine toolchain based on host OS
case "$(uname -s)" in
Darwin) HOST_TAG="darwin-x86_64" ;;
Linux) HOST_TAG="linux-x86_64" ;;
*) echo "Unsupported host OS"; exit 1 ;;
esac
TOOLCHAIN="$NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG"
# Set compiler based on architecture
case "{{.ARCH}}" in
arm64)
export CC="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang"
export CXX="$TOOLCHAIN/bin/aarch64-linux-android{{.MIN_SDK}}-clang++"
export GOARCH=arm64
JNI_DIR="arm64-v8a"
;;
amd64|x86_64)
export CC="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang"
export CXX="$TOOLCHAIN/bin/x86_64-linux-android{{.MIN_SDK}}-clang++"
export GOARCH=amd64
JNI_DIR="x86_64"
;;
*)
echo "Unsupported architecture: {{.ARCH}}"
exit 1
;;
esac
export CGO_ENABLED=1
export GOOS=android
mkdir -p {{.BIN_DIR}}
mkdir -p build/android/app/src/main/jniLibs/$JNI_DIR
go build -buildmode=c-shared {{.BUILD_FLAGS}} \
-o build/android/app/src/main/jniLibs/$JNI_DIR/libwails.so
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}'
compile:go:all-archs:
summary: Compile Go code for all Android architectures (fat APK)
cmds:
- task: compile:go:shared
vars:
ARCH: arm64
- task: compile:go:shared
vars:
ARCH: amd64
package:
summary: Packages a production build of the application into an APK
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- task: assemble:apk
package:fat:
summary: Packages a production build for all architectures (fat APK)
cmds:
- task: compile:go:all-archs
- task: assemble:apk
assemble:apk:
summary: Assembles the APK using Gradle
cmds:
- |
cd build/android
./gradlew assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}.apk"
echo "APK created: {{.BIN_DIR}}/{{.APP_NAME}}.apk"
assemble:apk:release:
summary: Assembles a release APK using Gradle
cmds:
- |
cd build/android
./gradlew assembleRelease
cp app/build/outputs/apk/release/app-release-unsigned.apk "../../{{.BIN_DIR}}/{{.APP_NAME}}-release.apk"
echo "Release APK created: {{.BIN_DIR}}/{{.APP_NAME}}-release.apk"
generate:android:bindings:
internal: true
summary: Generates bindings for Android
sources:
- "**/*.go"
- go.mod
- go.sum
generates:
- frontend/bindings/**/*
cmds:
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true
env:
GOOS: android
CGO_ENABLED: 1
GOARCH: '{{.ARCH | default "arm64"}}'
ensure-emulator:
internal: true
summary: Ensure Android Emulator is running
silent: true
cmds:
- |
# Check if an emulator is already running
if adb devices | grep -q "emulator"; then
echo "Emulator already running"
exit 0
fi
# Get first available AVD
AVD_NAME=$(emulator -list-avds | head -1)
if [ -z "$AVD_NAME" ]; then
echo "No Android Virtual Devices found."
echo "Create one using: Android Studio > Tools > Device Manager"
exit 1
fi
echo "Starting emulator: $AVD_NAME"
emulator -avd "$AVD_NAME" -no-snapshot-load &
# Wait for emulator to boot (max 60 seconds)
echo "Waiting for emulator to boot..."
adb wait-for-device
for i in {1..60}; do
BOOT_COMPLETED=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')
if [ "$BOOT_COMPLETED" = "1" ]; then
echo "Emulator booted successfully"
exit 0
fi
sleep 1
done
echo "Emulator boot timeout"
exit 1
preconditions:
- sh: command -v adb
msg: "adb not found. Please install Android SDK and add platform-tools to PATH"
- sh: command -v emulator
msg: "emulator not found. Please install Android SDK and add emulator to PATH"
deploy-emulator:
summary: Deploy to Android Emulator
deps: [package]
cmds:
- adb uninstall {{.APP_ID}} 2>/dev/null || true
- adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk"
- adb shell am start -n {{.APP_ID}}/.MainActivity
run:
summary: Run the application in Android Emulator
deps:
- task: ensure-emulator
- task: build
vars:
ARCH: x86_64
cmds:
- task: assemble:apk
- adb uninstall {{.APP_ID}} 2>/dev/null || true
- adb install "{{.BIN_DIR}}/{{.APP_NAME}}.apk"
- adb shell am start -n {{.APP_ID}}/.MainActivity
logs:
summary: Stream Android logcat filtered to this app
cmds:
- adb logcat -v time | grep -E "(Wails|{{.APP_NAME}})"
logs:all:
summary: Stream all Android logcat (verbose)
cmds:
- adb logcat -v time
clean:
summary: Clean build artifacts
cmds:
- rm -rf {{.BIN_DIR}}
- rm -rf build/android/app/build
- rm -rf build/android/app/src/main/jniLibs/*/libwails.so
- rm -rf build/android/.gradle

View file

@ -0,0 +1,63 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.wails.app'
compileSdk 34
buildFeatures {
buildConfig = true
}
defaultConfig {
applicationId "com.wails.app"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
// Configure supported ABIs
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
debuggable true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
// Source sets configuration
sourceSets {
main {
// JNI libraries are in jniLibs folder
jniLibs.srcDirs = ['src/main/jniLibs']
// Assets for the WebView
assets.srcDirs = ['src/main/assets']
}
}
// Packaging options
packagingOptions {
// Don't strip Go symbols in debug builds
doNotStrip '*/arm64-v8a/libwails.so'
doNotStrip '*/x86_64/libwails.so'
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.webkit:webkit:1.9.0'
implementation 'com.google.android.material:material:1.11.0'
}

12
build/android/app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,12 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# Keep native methods
-keepclasseswithmembernames class * {
native <methods>;
}
# Keep Wails bridge classes
-keep class com.wails.app.WailsBridge { *; }
-keep class com.wails.app.WailsJSBridge { *; }

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Internet permission for WebView -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WailsApp"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,198 @@
package com.wails.app;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.webkit.WebViewAssetLoader;
import com.wails.app.BuildConfig;
/**
* MainActivity hosts the WebView and manages the Wails application lifecycle.
* It uses WebViewAssetLoader to serve assets from the Go library without
* requiring a network server.
*/
public class MainActivity extends AppCompatActivity {
private static final String TAG = "WailsActivity";
private static final String WAILS_SCHEME = "https";
private static final String WAILS_HOST = "wails.localhost";
private WebView webView;
private WailsBridge bridge;
private WebViewAssetLoader assetLoader;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Initialize the native Go library
bridge = new WailsBridge(this);
bridge.initialize();
// Set up WebView
setupWebView();
// Load the application
loadApplication();
}
@SuppressLint("SetJavaScriptEnabled")
private void setupWebView() {
webView = findViewById(R.id.webview);
// Configure WebView settings
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setDatabaseEnabled(true);
settings.setAllowFileAccess(false);
settings.setAllowContentAccess(false);
settings.setMediaPlaybackRequiresUserGesture(false);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
// Enable debugging in debug builds
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true);
}
// Set up asset loader for serving local assets
assetLoader = new WebViewAssetLoader.Builder()
.setDomain(WAILS_HOST)
.addPathHandler("/", new WailsPathHandler(bridge))
.build();
// Set up WebView client to intercept requests
webView.setWebViewClient(new WebViewClient() {
@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
Log.d(TAG, "Intercepting request: " + url);
// Handle wails.localhost requests
if (request.getUrl().getHost() != null &&
request.getUrl().getHost().equals(WAILS_HOST)) {
// For wails API calls (runtime, capabilities, etc.), we need to pass the full URL
// including query string because WebViewAssetLoader.PathHandler strips query params
String path = request.getUrl().getPath();
if (path != null && path.startsWith("/wails/")) {
// Get full path with query string for runtime calls
String fullPath = path;
String query = request.getUrl().getQuery();
if (query != null && !query.isEmpty()) {
fullPath = path + "?" + query;
}
Log.d(TAG, "Wails API call detected, full path: " + fullPath);
// Call bridge directly with full path
byte[] data = bridge.serveAsset(fullPath, request.getMethod(), "{}");
if (data != null && data.length > 0) {
java.io.InputStream inputStream = new java.io.ByteArrayInputStream(data);
java.util.Map<String, String> headers = new java.util.HashMap<>();
headers.put("Access-Control-Allow-Origin", "*");
headers.put("Cache-Control", "no-cache");
headers.put("Content-Type", "application/json");
return new WebResourceResponse(
"application/json",
"UTF-8",
200,
"OK",
headers,
inputStream
);
}
// Return error response if data is null
return new WebResourceResponse(
"application/json",
"UTF-8",
500,
"Internal Error",
new java.util.HashMap<>(),
new java.io.ByteArrayInputStream("{}".getBytes())
);
}
// For regular assets, use the asset loader
return assetLoader.shouldInterceptRequest(request.getUrl());
}
return super.shouldInterceptRequest(view, request);
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.d(TAG, "Page loaded: " + url);
// Inject Wails runtime
bridge.injectRuntime(webView, url);
}
});
// Add JavaScript interface for Go communication
webView.addJavascriptInterface(new WailsJSBridge(bridge, webView), "wails");
}
private void loadApplication() {
// Load the main page from the asset server
String url = WAILS_SCHEME + "://" + WAILS_HOST + "/";
Log.d(TAG, "Loading URL: " + url);
webView.loadUrl(url);
}
/**
* Execute JavaScript in the WebView from the Go side
*/
public void executeJavaScript(final String js) {
runOnUiThread(() -> {
if (webView != null) {
webView.evaluateJavascript(js, null);
}
});
}
@Override
protected void onResume() {
super.onResume();
if (bridge != null) {
bridge.onResume();
}
}
@Override
protected void onPause() {
super.onPause();
if (bridge != null) {
bridge.onPause();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (bridge != null) {
bridge.shutdown();
}
if (webView != null) {
webView.destroy();
}
}
@Override
public void onBackPressed() {
if (webView != null && webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
}
}

View file

@ -0,0 +1,214 @@
package com.wails.app;
import android.content.Context;
import android.util.Log;
import android.webkit.WebView;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* WailsBridge manages the connection between the Java/Android side and the Go native library.
* It handles:
* - Loading and initializing the native Go library
* - Serving asset requests from Go
* - Passing messages between JavaScript and Go
* - Managing callbacks for async operations
*/
public class WailsBridge {
private static final String TAG = "WailsBridge";
static {
// Load the native Go library
System.loadLibrary("wails");
}
private final Context context;
private final AtomicInteger callbackIdGenerator = new AtomicInteger(0);
private final ConcurrentHashMap<Integer, AssetCallback> pendingAssetCallbacks = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, MessageCallback> pendingMessageCallbacks = new ConcurrentHashMap<>();
private WebView webView;
private volatile boolean initialized = false;
// Native methods - implemented in Go
private static native void nativeInit(WailsBridge bridge);
private static native void nativeShutdown();
private static native void nativeOnResume();
private static native void nativeOnPause();
private static native void nativeOnPageFinished(String url);
private static native byte[] nativeServeAsset(String path, String method, String headers);
private static native String nativeHandleMessage(String message);
private static native String nativeGetAssetMimeType(String path);
public WailsBridge(Context context) {
this.context = context;
}
/**
* Initialize the native Go library
*/
public void initialize() {
if (initialized) {
return;
}
Log.i(TAG, "Initializing Wails bridge...");
try {
nativeInit(this);
initialized = true;
Log.i(TAG, "Wails bridge initialized successfully");
} catch (Exception e) {
Log.e(TAG, "Failed to initialize Wails bridge", e);
}
}
/**
* Shutdown the native Go library
*/
public void shutdown() {
if (!initialized) {
return;
}
Log.i(TAG, "Shutting down Wails bridge...");
try {
nativeShutdown();
initialized = false;
} catch (Exception e) {
Log.e(TAG, "Error during shutdown", e);
}
}
/**
* Called when the activity resumes
*/
public void onResume() {
if (initialized) {
nativeOnResume();
}
}
/**
* Called when the activity pauses
*/
public void onPause() {
if (initialized) {
nativeOnPause();
}
}
/**
* Serve an asset from the Go asset server
* @param path The URL path requested
* @param method The HTTP method
* @param headers The request headers as JSON
* @return The asset data, or null if not found
*/
public byte[] serveAsset(String path, String method, String headers) {
if (!initialized) {
Log.w(TAG, "Bridge not initialized, cannot serve asset: " + path);
return null;
}
Log.d(TAG, "Serving asset: " + path);
try {
return nativeServeAsset(path, method, headers);
} catch (Exception e) {
Log.e(TAG, "Error serving asset: " + path, e);
return null;
}
}
/**
* Get the MIME type for an asset
* @param path The asset path
* @return The MIME type string
*/
public String getAssetMimeType(String path) {
if (!initialized) {
return "application/octet-stream";
}
try {
String mimeType = nativeGetAssetMimeType(path);
return mimeType != null ? mimeType : "application/octet-stream";
} catch (Exception e) {
Log.e(TAG, "Error getting MIME type for: " + path, e);
return "application/octet-stream";
}
}
/**
* Handle a message from JavaScript
* @param message The message from JavaScript (JSON)
* @return The response to send back to JavaScript (JSON)
*/
public String handleMessage(String message) {
if (!initialized) {
Log.w(TAG, "Bridge not initialized, cannot handle message");
return "{\"error\":\"Bridge not initialized\"}";
}
Log.d(TAG, "Handling message from JS: " + message);
try {
return nativeHandleMessage(message);
} catch (Exception e) {
Log.e(TAG, "Error handling message", e);
return "{\"error\":\"" + e.getMessage() + "\"}";
}
}
/**
* Inject the Wails runtime JavaScript into the WebView.
* Called when the page finishes loading.
* @param webView The WebView to inject into
* @param url The URL that finished loading
*/
public void injectRuntime(WebView webView, String url) {
this.webView = webView;
// Notify Go side that page has finished loading so it can inject the runtime
Log.d(TAG, "Page finished loading: " + url + ", notifying Go side");
if (initialized) {
nativeOnPageFinished(url);
}
}
/**
* Execute JavaScript in the WebView (called from Go side)
* @param js The JavaScript code to execute
*/
public void executeJavaScript(String js) {
if (webView != null) {
webView.post(() -> webView.evaluateJavascript(js, null));
}
}
/**
* Called from Go when an event needs to be emitted to JavaScript
* @param eventName The event name
* @param eventData The event data (JSON)
*/
public void emitEvent(String eventName, String eventData) {
String js = String.format("window.wails && window.wails._emit('%s', %s);",
escapeJsString(eventName), eventData);
executeJavaScript(js);
}
private String escapeJsString(String str) {
return str.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\n", "\\n")
.replace("\r", "\\r");
}
// Callback interfaces
public interface AssetCallback {
void onAssetReady(byte[] data, String mimeType);
void onAssetError(String error);
}
public interface MessageCallback {
void onResponse(String response);
void onError(String error);
}
}

View file

@ -0,0 +1,142 @@
package com.wails.app;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import com.wails.app.BuildConfig;
/**
* WailsJSBridge provides the JavaScript interface that allows the web frontend
* to communicate with the Go backend. This is exposed to JavaScript as the
* `window.wails` object.
*
* Similar to iOS's WKScriptMessageHandler but using Android's addJavascriptInterface.
*/
public class WailsJSBridge {
private static final String TAG = "WailsJSBridge";
private final WailsBridge bridge;
private final WebView webView;
public WailsJSBridge(WailsBridge bridge, WebView webView) {
this.bridge = bridge;
this.webView = webView;
}
/**
* Send a message to Go and return the response synchronously.
* Called from JavaScript: wails.invoke(message)
*
* @param message The message to send (JSON string)
* @return The response from Go (JSON string)
*/
@JavascriptInterface
public String invoke(String message) {
Log.d(TAG, "Invoke called: " + message);
return bridge.handleMessage(message);
}
/**
* Send a message to Go asynchronously.
* The response will be sent back via a callback.
* Called from JavaScript: wails.invokeAsync(callbackId, message)
*
* @param callbackId The callback ID to use for the response
* @param message The message to send (JSON string)
*/
@JavascriptInterface
public void invokeAsync(final String callbackId, final String message) {
Log.d(TAG, "InvokeAsync called: " + message);
// Handle in background thread to not block JavaScript
new Thread(() -> {
try {
String response = bridge.handleMessage(message);
sendCallback(callbackId, response, null);
} catch (Exception e) {
Log.e(TAG, "Error in async invoke", e);
sendCallback(callbackId, null, e.getMessage());
}
}).start();
}
/**
* Log a message from JavaScript to Android's logcat
* Called from JavaScript: wails.log(level, message)
*
* @param level The log level (debug, info, warn, error)
* @param message The message to log
*/
@JavascriptInterface
public void log(String level, String message) {
switch (level.toLowerCase()) {
case "debug":
Log.d(TAG + "/JS", message);
break;
case "info":
Log.i(TAG + "/JS", message);
break;
case "warn":
Log.w(TAG + "/JS", message);
break;
case "error":
Log.e(TAG + "/JS", message);
break;
default:
Log.v(TAG + "/JS", message);
break;
}
}
/**
* Get the platform name
* Called from JavaScript: wails.platform()
*
* @return "android"
*/
@JavascriptInterface
public String platform() {
return "android";
}
/**
* Check if we're running in debug mode
* Called from JavaScript: wails.isDebug()
*
* @return true if debug build, false otherwise
*/
@JavascriptInterface
public boolean isDebug() {
return BuildConfig.DEBUG;
}
/**
* Send a callback response to JavaScript
*/
private void sendCallback(String callbackId, String result, String error) {
final String js;
if (error != null) {
js = String.format(
"window.wails && window.wails._callback('%s', null, '%s');",
escapeJsString(callbackId),
escapeJsString(error)
);
} else {
js = String.format(
"window.wails && window.wails._callback('%s', %s, null);",
escapeJsString(callbackId),
result != null ? result : "null"
);
}
webView.post(() -> webView.evaluateJavascript(js, null));
}
private String escapeJsString(String str) {
if (str == null) return "";
return str.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\n", "\\n")
.replace("\r", "\\r");
}
}

View file

@ -0,0 +1,118 @@
package com.wails.app;
import android.net.Uri;
import android.util.Log;
import android.webkit.WebResourceResponse;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.WebViewAssetLoader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/**
* WailsPathHandler implements WebViewAssetLoader.PathHandler to serve assets
* from the Go asset server. This allows the WebView to load assets without
* using a network server, similar to iOS's WKURLSchemeHandler.
*/
public class WailsPathHandler implements WebViewAssetLoader.PathHandler {
private static final String TAG = "WailsPathHandler";
private final WailsBridge bridge;
public WailsPathHandler(WailsBridge bridge) {
this.bridge = bridge;
}
@Nullable
@Override
public WebResourceResponse handle(@NonNull String path) {
Log.d(TAG, "Handling path: " + path);
// Normalize path
if (path.isEmpty() || path.equals("/")) {
path = "/index.html";
}
// Get asset from Go
byte[] data = bridge.serveAsset(path, "GET", "{}");
if (data == null || data.length == 0) {
Log.w(TAG, "Asset not found: " + path);
return null; // Return null to let WebView handle 404
}
// Determine MIME type
String mimeType = bridge.getAssetMimeType(path);
Log.d(TAG, "Serving " + path + " with type " + mimeType + " (" + data.length + " bytes)");
// Create response
InputStream inputStream = new ByteArrayInputStream(data);
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*");
headers.put("Cache-Control", "no-cache");
return new WebResourceResponse(
mimeType,
"UTF-8",
200,
"OK",
headers,
inputStream
);
}
/**
* Determine MIME type from file extension
*/
private String getMimeType(String path) {
String lowerPath = path.toLowerCase();
if (lowerPath.endsWith(".html") || lowerPath.endsWith(".htm")) {
return "text/html";
} else if (lowerPath.endsWith(".js") || lowerPath.endsWith(".mjs")) {
return "application/javascript";
} else if (lowerPath.endsWith(".css")) {
return "text/css";
} else if (lowerPath.endsWith(".json")) {
return "application/json";
} else if (lowerPath.endsWith(".png")) {
return "image/png";
} else if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) {
return "image/jpeg";
} else if (lowerPath.endsWith(".gif")) {
return "image/gif";
} else if (lowerPath.endsWith(".svg")) {
return "image/svg+xml";
} else if (lowerPath.endsWith(".ico")) {
return "image/x-icon";
} else if (lowerPath.endsWith(".woff")) {
return "font/woff";
} else if (lowerPath.endsWith(".woff2")) {
return "font/woff2";
} else if (lowerPath.endsWith(".ttf")) {
return "font/ttf";
} else if (lowerPath.endsWith(".eot")) {
return "application/vnd.ms-fontobject";
} else if (lowerPath.endsWith(".xml")) {
return "application/xml";
} else if (lowerPath.endsWith(".txt")) {
return "text/plain";
} else if (lowerPath.endsWith(".wasm")) {
return "application/wasm";
} else if (lowerPath.endsWith(".mp3")) {
return "audio/mpeg";
} else if (lowerPath.endsWith(".mp4")) {
return "video/mp4";
} else if (lowerPath.endsWith(".webm")) {
return "video/webm";
} else if (lowerPath.endsWith(".webp")) {
return "image/webp";
}
return "application/octet-stream";
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="wails_blue">#3574D4</color>
<color name="wails_blue_dark">#2C5FB8</color>
<color name="wails_background">#1B2636</color>
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Wails App</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.WailsApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/wails_blue</item>
<item name="colorPrimaryVariant">@color/wails_blue_dark</item>
<item name="colorOnPrimary">@android:color/white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">@color/wails_background</item>
<item name="android:navigationBarColor">@color/wails_background</item>
<!-- Window background -->
<item name="android:windowBackground">@color/wails_background</item>
</style>
</resources>

View file

@ -0,0 +1,4 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.7.3' apply false
}

View file

@ -0,0 +1,26 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/build/optimize-your-build#parallel
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

Binary file not shown.

View file

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

248
build/android/gradlew vendored Normal file
View file

@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

93
build/android/gradlew.bat vendored Normal file
View file

@ -0,0 +1,93 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1,11 @@
//go:build android
package main
import "github.com/wailsapp/wails/v3/pkg/application"
func init() {
// Register main function to be called when the Android app initializes
// This is necessary because in c-shared build mode, main() is not automatically called
application.RegisterAndroidMain(main)
}

View file

@ -0,0 +1,151 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
func main() {
fmt.Println("Checking Android development dependencies...")
fmt.Println()
errors := []string{}
// Check Go
if !checkCommand("go", "version") {
errors = append(errors, "Go is not installed. Install from https://go.dev/dl/")
} else {
fmt.Println("✓ Go is installed")
}
// Check ANDROID_HOME
androidHome := os.Getenv("ANDROID_HOME")
if androidHome == "" {
androidHome = os.Getenv("ANDROID_SDK_ROOT")
}
if androidHome == "" {
// Try common default locations
home, _ := os.UserHomeDir()
possiblePaths := []string{
filepath.Join(home, "Android", "Sdk"),
filepath.Join(home, "Library", "Android", "sdk"),
"/usr/local/share/android-sdk",
}
for _, p := range possiblePaths {
if _, err := os.Stat(p); err == nil {
androidHome = p
break
}
}
}
if androidHome == "" {
errors = append(errors, "ANDROID_HOME not set. Install Android Studio and set ANDROID_HOME environment variable")
} else {
fmt.Printf("✓ ANDROID_HOME: %s\n", androidHome)
}
// Check adb
if !checkCommand("adb", "version") {
if androidHome != "" {
platformTools := filepath.Join(androidHome, "platform-tools")
errors = append(errors, fmt.Sprintf("adb not found. Add %s to PATH", platformTools))
} else {
errors = append(errors, "adb not found. Install Android SDK Platform-Tools")
}
} else {
fmt.Println("✓ adb is installed")
}
// Check emulator
if !checkCommand("emulator", "-list-avds") {
if androidHome != "" {
emulatorPath := filepath.Join(androidHome, "emulator")
errors = append(errors, fmt.Sprintf("emulator not found. Add %s to PATH", emulatorPath))
} else {
errors = append(errors, "emulator not found. Install Android Emulator via SDK Manager")
}
} else {
fmt.Println("✓ Android Emulator is installed")
}
// Check NDK
ndkHome := os.Getenv("ANDROID_NDK_HOME")
if ndkHome == "" && androidHome != "" {
// Look for NDK in default location
ndkDir := filepath.Join(androidHome, "ndk")
if entries, err := os.ReadDir(ndkDir); err == nil {
for _, entry := range entries {
if entry.IsDir() {
ndkHome = filepath.Join(ndkDir, entry.Name())
break
}
}
}
}
if ndkHome == "" {
errors = append(errors, "Android NDK not found. Install NDK via Android Studio > SDK Manager > SDK Tools > NDK (Side by side)")
} else {
fmt.Printf("✓ Android NDK: %s\n", ndkHome)
}
// Check Java
if !checkCommand("java", "-version") {
errors = append(errors, "Java not found. Install JDK 11+ (OpenJDK recommended)")
} else {
fmt.Println("✓ Java is installed")
}
// Check for AVD (Android Virtual Device)
if checkCommand("emulator", "-list-avds") {
cmd := exec.Command("emulator", "-list-avds")
output, err := cmd.Output()
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
avds := strings.Split(strings.TrimSpace(string(output)), "\n")
fmt.Printf("✓ Found %d Android Virtual Device(s)\n", len(avds))
} else {
fmt.Println("⚠ No Android Virtual Devices found. Create one via Android Studio > Tools > Device Manager")
}
}
fmt.Println()
if len(errors) > 0 {
fmt.Println("❌ Missing dependencies:")
for _, err := range errors {
fmt.Printf(" - %s\n", err)
}
fmt.Println()
fmt.Println("Setup instructions:")
fmt.Println("1. Install Android Studio: https://developer.android.com/studio")
fmt.Println("2. Open SDK Manager and install:")
fmt.Println(" - Android SDK Platform (API 34)")
fmt.Println(" - Android SDK Build-Tools")
fmt.Println(" - Android SDK Platform-Tools")
fmt.Println(" - Android Emulator")
fmt.Println(" - NDK (Side by side)")
fmt.Println("3. Set environment variables:")
if runtime.GOOS == "darwin" {
fmt.Println(" export ANDROID_HOME=$HOME/Library/Android/sdk")
} else {
fmt.Println(" export ANDROID_HOME=$HOME/Android/Sdk")
}
fmt.Println(" export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator")
fmt.Println("4. Create an AVD via Android Studio > Tools > Device Manager")
os.Exit(1)
}
fmt.Println("✓ All Android development dependencies are installed!")
}
func checkCommand(name string, args ...string) bool {
cmd := exec.Command(name, args...)
cmd.Stdout = nil
cmd.Stderr = nil
return cmd.Run() == nil
}

View file

@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "WailsApp"
include ':app'

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 583 533" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-246,-251)">
<g id="Ebene1">
<path d="M246,251L265,784L401,784L506,450L507,450L505,784L641,784L829,251L682,251L596,567L595,567L596,251L478,251L378,568L391,251L246,251Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 698 B

View file

@ -0,0 +1,51 @@
{
"fill" : {
"automatic-gradient" : "extended-gray:1.00000,1.00000"
},
"groups" : [
{
"layers" : [
{
"fill-specializations" : [
{
"appearance" : "dark",
"value" : {
"solid" : "srgb:0.92143,0.92145,0.92144,1.00000"
}
},
{
"appearance" : "tinted",
"value" : {
"solid" : "srgb:0.83742,0.83744,0.83743,1.00000"
}
}
],
"image-name" : "wails_icon_vector.svg",
"name" : "wails_icon_vector",
"position" : {
"scale" : 1.25,
"translation-in-points" : [
36.890625,
4.96875
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View file

@ -30,15 +30,15 @@ dev_mode:
- .gitkeep - .gitkeep
watched_extension: watched_extension:
- "*.go" - "*.go"
- "*.js"
- "*.ts"
git_ignore: true git_ignore: true
executes: executes:
- cmd: wails3 task common:install:frontend:deps - cmd: wails3 task common:install:frontend:deps
type: once type: once
- cmd: wails3 task common:dev:frontend - cmd: wails3 task common:dev:frontend
type: background type: background
- cmd: go mod tidy - cmd: wails3 build DEV=true
type: blocking
- cmd: wails3 task build
type: blocking type: blocking
- cmd: wails3 task run - cmd: wails3 task run
type: primary type: primary

View file

@ -3,22 +3,45 @@ version: '3'
includes: includes:
common: ../Taskfile.yml common: ../Taskfile.yml
vars:
# Signing configuration - edit these values for your project
# SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)"
# KEYCHAIN_PROFILE: "my-notarize-profile"
# ENTITLEMENTS: "build/darwin/entitlements.plist"
# Docker image for cross-compilation (used when building on non-macOS)
CROSS_IMAGE: wails-cross
tasks: tasks:
build: build:
summary: Creates a production build of the application summary: Builds the application
cmds:
- task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}'
vars:
ARCH: '{{.ARCH}}'
DEV: '{{.DEV}}'
OUTPUT: '{{.OUTPUT}}'
EXTRA_TAGS: '{{.EXTRA_TAGS}}'
vars:
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
build:native:
summary: Builds the application natively on macOS
internal: true
deps: deps:
- task: common:go:mod:tidy - task: common:go:mod:tidy
- task: common:build:frontend - task: common:build:frontend
vars: vars:
BUILD_FLAGS: BUILD_FLAGS:
ref: .BUILD_FLAGS ref: .BUILD_FLAGS
PRODUCTION: DEV:
ref: .PRODUCTION ref: .DEV
- task: common:generate:icons - task: common:generate:icons
cmds: cmds:
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
vars: vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
env: env:
@ -28,7 +51,47 @@ tasks:
CGO_CFLAGS: "-mmacosx-version-min=10.15" CGO_CFLAGS: "-mmacosx-version-min=10.15"
CGO_LDFLAGS: "-mmacosx-version-min=10.15" CGO_LDFLAGS: "-mmacosx-version-min=10.15"
MACOSX_DEPLOYMENT_TARGET: "10.15" MACOSX_DEPLOYMENT_TARGET: "10.15"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:docker:
summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts)
internal: true
deps:
- task: common:build:frontend
- task: common:generate:icons
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required for cross-compilation. Please install Docker."
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
msg: |
Docker image '{{.CROSS_IMAGE}}' not found.
Build it first: wails3 task setup:docker
cmds:
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
- mkdir -p {{.BIN_DIR}}
- mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
vars:
DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Mount Go module cache for faster builds
GO_CACHE_MOUNT:
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
# Extract replace directives from go.mod and create -v mounts for each
# Handles both relative (=> ../) and absolute (=> /) paths
REPLACE_MOUNTS:
sh: |
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
# Convert relative paths to absolute
if [ "${path#/}" = "$path" ]; then
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
fi
# Only mount if directory exists
if [ -d "$path" ]; then
echo "-v $path:$path:ro"
fi
done | tr '\n' ' '
build:universal: build:universal:
summary: Builds darwin universal binary (arm64 + amd64) summary: Builds darwin universal binary (arm64 + amd64)
@ -37,22 +100,31 @@ tasks:
vars: vars:
ARCH: amd64 ARCH: amd64
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64"
PRODUCTION: '{{.PRODUCTION | default "true"}}'
- task: build - task: build
vars: vars:
ARCH: arm64 ARCH: arm64
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
PRODUCTION: '{{.PRODUCTION | default "true"}}' cmds:
- task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}'
build:universal:lipo:native:
summary: Creates universal binary using native lipo (macOS)
internal: true
cmds: cmds:
- lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
- rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
build:universal:lipo:go:
summary: Creates universal binary using wails3 tool lipo (Linux/Windows)
internal: true
cmds:
- wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
- rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
package: package:
summary: Packages a production build of the application into a `.app` bundle summary: Packages the application into a `.app` bundle
deps: deps:
- task: build - task: build
vars:
PRODUCTION: "true"
cmds: cmds:
- task: create:app:bundle - task: create:app:bundle
@ -67,19 +139,70 @@ tasks:
create:app:bundle: create:app:bundle:
summary: Creates an `.app` bundle summary: Creates an `.app` bundle
cmds: cmds:
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
- cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents - |
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app if [ -f build/darwin/Assets.car ]; then
cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
fi
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
- cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents"
- task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}'
codesign:adhoc:
summary: Ad-hoc signs the app bundle (macOS only)
internal: true
cmds:
- codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app"
codesign:skip:
summary: Skips codesigning when cross-compiling
internal: true
cmds:
- 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."'
run: run:
deps:
- task: build
cmds: cmds:
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources} - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources - mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS - cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
- cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist - |
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app if [ -f build/darwin/Assets.car ]; then
cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
fi
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
- cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist"
- codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}' - '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}'
sign:
summary: Signs the application bundle with Developer ID
desc: |
Signs the .app bundle for distribution.
Configure SIGN_IDENTITY in the vars section at the top of this file.
deps:
- task: package
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
sign:notarize:
summary: Signs and notarizes the application bundle
desc: |
Signs the .app bundle and submits it for notarization.
Configure SIGN_IDENTITY and KEYCHAIN_PROFILE in the vars section at the top of this file.
Setup (one-time):
wails3 signing credentials --apple-id "you@email.com" --team-id "TEAMID" --password "app-specific-password" --profile "my-profile"
deps:
- task: package
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}}
preconditions:
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
- sh: '[ -n "{{.KEYCHAIN_PROFILE}}" ]'
msg: "KEYCHAIN_PROFILE is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"

View file

@ -0,0 +1,203 @@
# Cross-compile Wails v3 apps to any platform
#
# Darwin: Zig + macOS SDK
# Linux: Native GCC when host matches target, Zig for cross-arch
# Windows: Zig + bundled mingw
#
# Usage:
# docker build -t wails-cross -f Dockerfile.cross .
# docker run --rm -v $(pwd):/app wails-cross darwin arm64
# docker run --rm -v $(pwd):/app wails-cross darwin amd64
# docker run --rm -v $(pwd):/app wails-cross linux amd64
# docker run --rm -v $(pwd):/app wails-cross linux arm64
# docker run --rm -v $(pwd):/app wails-cross windows amd64
# docker run --rm -v $(pwd):/app wails-cross windows arm64
FROM golang:1.25-bookworm
ARG TARGETARCH
# Install base tools, GCC, and GTK/WebKit dev packages
RUN apt-get update && apt-get install -y --no-install-recommends \
curl xz-utils nodejs npm pkg-config gcc libc6-dev \
libgtk-3-dev libwebkit2gtk-4.1-dev \
libgtk-4-dev libwebkitgtk-6.0-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Zig - automatically selects correct binary for host architecture
ARG ZIG_VERSION=0.14.0
RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" \
| tar -xJ -C /opt \
&& ln -s /opt/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}/zig /usr/local/bin/zig
# Download macOS SDK (required for darwin targets)
ARG MACOS_SDK_VERSION=14.5
RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \
| tar -xJ -C /opt \
&& mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk
ENV MACOS_SDK_PATH=/opt/macos-sdk
# Create Zig CC wrappers for cross-compilation targets
# Darwin and Windows use Zig; Linux uses native GCC (run with --platform for cross-arch)
# Darwin arm64
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-mmacosx-version-min=*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-darwin-arm64
# Darwin amd64
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-mmacosx-version-min=*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-darwin-amd64
# Windows amd64 - uses Zig's bundled mingw
COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-Wl,*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target x86_64-windows-gnu $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-windows-amd64
# Windows arm64 - uses Zig's bundled mingw
COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-Wl,*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target aarch64-windows-gnu $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-windows-arm64
# Build script
COPY <<'SCRIPT' /usr/local/bin/build.sh
#!/bin/sh
set -e
OS=${1:-darwin}
ARCH=${2:-arm64}
case "${OS}-${ARCH}" in
darwin-arm64|darwin-aarch64)
export CC=zcc-darwin-arm64
export GOARCH=arm64
export GOOS=darwin
;;
darwin-amd64|darwin-x86_64)
export CC=zcc-darwin-amd64
export GOARCH=amd64
export GOOS=darwin
;;
linux-arm64|linux-aarch64)
export CC=gcc
export GOARCH=arm64
export GOOS=linux
;;
linux-amd64|linux-x86_64)
export CC=gcc
export GOARCH=amd64
export GOOS=linux
;;
windows-arm64|windows-aarch64)
export CC=zcc-windows-arm64
export GOARCH=arm64
export GOOS=windows
;;
windows-amd64|windows-x86_64)
export CC=zcc-windows-amd64
export GOARCH=amd64
export GOOS=windows
;;
*)
echo "Usage: <os> <arch>"
echo " os: darwin, linux, windows"
echo " arch: amd64, arm64"
exit 1
;;
esac
export CGO_ENABLED=1
export CGO_CFLAGS="-w"
# Build frontend if exists and not already built (host may have built it)
if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then
(cd frontend && npm install --silent && npm run build --silent)
fi
# Build
APP=${APP_NAME:-$(basename $(pwd))}
mkdir -p bin
EXT=""
LDFLAGS="-s -w"
if [ "$GOOS" = "windows" ]; then
EXT=".exe"
LDFLAGS="-s -w -H windowsgui"
fi
TAGS="production"
if [ -n "$EXTRA_TAGS" ]; then
TAGS="${TAGS},${EXTRA_TAGS}"
fi
go build -tags "$TAGS" -trimpath -buildvcs=false -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} .
echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}"
SCRIPT
RUN chmod +x /usr/local/bin/build.sh
WORKDIR /app
ENTRYPOINT ["/usr/local/bin/build.sh"]
CMD ["darwin", "arm64"]

View file

@ -0,0 +1,41 @@
# Wails Server Mode Dockerfile
# Multi-stage build for minimal image size
# Build stage
FROM golang:alpine AS builder
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache git
# Copy source code
COPY . .
# Remove local replace directive if present (for production builds)
RUN sed -i '/^replace/d' go.mod || true
# Download dependencies
RUN go mod tidy
# Build the server binary
RUN go build -tags server -ldflags="-s -w" -o server .
# Runtime stage - minimal image
FROM gcr.io/distroless/static-debian12
# Copy the binary
COPY --from=builder /app/server /server
# Copy frontend assets
COPY --from=builder /app/frontend/dist /frontend/dist
# Expose the default port
EXPOSE 8080
# Bind to all interfaces (required for Docker)
# Can be overridden at runtime with -e WAILS_SERVER_HOST=...
ENV WAILS_SERVER_HOST=0.0.0.0
# Run the server
ENTRYPOINT ["/server"]

116
build/ios/Assets.xcassets Normal file
View file

@ -0,0 +1,116 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"images" : [
{
"filename" : "icon-20@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon-20@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "icon-29@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon-29@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "icon-40@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon-40@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "icon-60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "icon-60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "icon-20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "icon-20@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon-29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "icon-29@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon-40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "icon-40@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon-76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "icon-76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "icon-83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "icon-1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
]
}

62
build/ios/Info.dev.plist Normal file
View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>core-ide-test</string>
<key>CFBundleIdentifier</key>
<string>com.example.coreidetest.dev</string>
<key>CFBundleName</key>
<string>My Product (Dev)</string>
<key>CFBundleDisplayName</key>
<string>My Product (Dev)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0-dev</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>15.0</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<!-- Development mode enabled -->
<key>WailsDevelopmentMode</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>© 2026, My Company</string>
<key>CFBundleGetInfoString</key>
<string>This is a comment</string>
</dict>
</plist>

59
build/ios/Info.plist Normal file
View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>core-ide-test</string>
<key>CFBundleIdentifier</key>
<string>com.example.coreidetest</string>
<key>CFBundleName</key>
<string>My Product</string>
<key>CFBundleDisplayName</key>
<string>My Product</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>15.0</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSHumanReadableCopyright</key>
<string>© 2026, My Company</string>
<key>CFBundleGetInfoString</key>
<string>This is a comment</string>
</dict>
</plist>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="My Product" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
<rect key="frame" x="0.0" y="397" width="393" height="43"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="A core-ide-test application" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="MN2-I3-ftu">
<rect key="frame" x="0.0" y="448" width="393" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/2" constant="-20" id="moa-c2-u7t"/>
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="x7j-FC-K8j"/>
<constraint firstItem="MN2-I3-ftu" firstAttribute="top" secondItem="GJd-Yh-RWb" secondAttribute="bottom" constant="8" symbolic="YES" id="cPy-rs-vsC"/>
<constraint firstItem="MN2-I3-ftu" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="OQL-iM-xY6"/>
<constraint firstItem="MN2-I3-ftu" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="Dti-5h-tvW"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

293
build/ios/Taskfile.yml Normal file
View file

@ -0,0 +1,293 @@
version: '3'
includes:
common: ../Taskfile.yml
vars:
BUNDLE_ID: '{{.BUNDLE_ID | default "com.wails.app"}}'
# SDK_PATH is computed lazily at task-level to avoid errors on non-macOS systems
# Each task that needs it defines SDK_PATH in its own vars section
tasks:
install:deps:
summary: Check and install iOS development dependencies
cmds:
- go run build/ios/scripts/deps/install_deps.go
env:
TASK_FORCE_YES: '{{if .YES}}true{{else}}false{{end}}'
prompt: This will check and install iOS development dependencies. Continue?
# Note: Bindings generation may show CGO warnings for iOS C imports.
# These warnings are harmless and don't affect the generated bindings,
# as the generator only needs to parse Go types, not C implementations.
build:
summary: Creates a build of the application for iOS
deps:
- task: generate:ios:overlay
- task: generate:ios:xcode
- task: common:go:mod:tidy
- task: generate:ios:bindings
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
PRODUCTION:
ref: .PRODUCTION
- task: common:generate:icons
cmds:
- echo "Building iOS app {{.APP_NAME}}..."
- go build -buildmode=c-archive -overlay build/ios/xcode/overlay.json {{.BUILD_FLAGS}} -o {{.OUTPUT}}.a
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,ios -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags ios,debug -buildvcs=false -gcflags=all="-l"{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
SDK_PATH:
sh: xcrun --sdk iphonesimulator --show-sdk-path
env:
GOOS: ios
CGO_ENABLED: 1
GOARCH: '{{.ARCH | default "arm64"}}'
PRODUCTION: '{{.PRODUCTION | default "false"}}'
CGO_CFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0'
CGO_LDFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator'
compile:objc:
summary: Compile Objective-C iOS wrapper
vars:
SDK_PATH:
sh: xcrun --sdk iphonesimulator --show-sdk-path
cmds:
- xcrun -sdk iphonesimulator clang -target arm64-apple-ios15.0-simulator -isysroot {{.SDK_PATH}} -framework Foundation -framework UIKit -framework WebKit -o {{.BIN_DIR}}/{{.APP_NAME}} build/ios/main.m
- codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}"
package:
summary: Packages a production build of the application into a `.app` bundle
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- task: create:app:bundle
create:app:bundle:
summary: Creates an iOS `.app` bundle
cmds:
- rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.app"
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app"
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/"
- cp build/ios/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/"
- |
# Compile asset catalog and embed icons in the app bundle
APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.app"
AC_IN="build/ios/xcode/main/Assets.xcassets"
if [ -d "$AC_IN" ]; then
TMP_AC=$(mktemp -d)
xcrun actool \
--compile "$TMP_AC" \
--app-icon AppIcon \
--platform iphonesimulator \
--minimum-deployment-target 15.0 \
--product-type com.apple.product-type.application \
--target-device iphone \
--target-device ipad \
--output-partial-info-plist "$APP_BUNDLE/assetcatalog_generated_info.plist" \
"$AC_IN"
if [ -f "$TMP_AC/Assets.car" ]; then
cp -f "$TMP_AC/Assets.car" "$APP_BUNDLE/Assets.car"
fi
rm -rf "$TMP_AC"
if [ -f "$APP_BUNDLE/assetcatalog_generated_info.plist" ]; then
/usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true
fi
fi
- codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app"
deploy-simulator:
summary: Deploy to iOS Simulator
deps: [package]
cmds:
- xcrun simctl terminate booted {{.BUNDLE_ID}} 2>/dev/null || true
- xcrun simctl uninstall booted {{.BUNDLE_ID}} 2>/dev/null || true
- xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.app"
- xcrun simctl launch booted {{.BUNDLE_ID}}
compile:ios:
summary: Compile the iOS executable from Go archive and main.m
deps:
- task: build
vars:
SDK_PATH:
sh: xcrun --sdk iphonesimulator --show-sdk-path
cmds:
- |
MAIN_M=build/ios/xcode/main/main.m
if [ ! -f "$MAIN_M" ]; then
MAIN_M=build/ios/main.m
fi
xcrun -sdk iphonesimulator clang \
-target arm64-apple-ios15.0-simulator \
-isysroot {{.SDK_PATH}} \
-framework Foundation -framework UIKit -framework WebKit \
-framework Security -framework CoreFoundation \
-lresolv \
-o "{{.BIN_DIR}}/{{.APP_NAME | lower}}" \
"$MAIN_M" "{{.BIN_DIR}}/{{.APP_NAME}}.a"
generate:ios:bindings:
internal: true
summary: Generates bindings for iOS with proper CGO flags
sources:
- "**/*.go"
- go.mod
- go.sum
generates:
- frontend/bindings/**/*
vars:
SDK_PATH:
sh: xcrun --sdk iphonesimulator --show-sdk-path
cmds:
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true
env:
GOOS: ios
CGO_ENABLED: 1
GOARCH: '{{.ARCH | default "arm64"}}'
CGO_CFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0'
CGO_LDFLAGS: '-isysroot {{.SDK_PATH}} -target arm64-apple-ios15.0-simulator'
ensure-simulator:
internal: true
summary: Ensure iOS Simulator is running and booted
silent: true
cmds:
- |
if ! xcrun simctl list devices booted | grep -q "Booted"; then
echo "Starting iOS Simulator..."
# Get first available iPhone device
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone" | head -1 | grep -o "[A-F0-9-]\{36\}" || true)
if [ -z "$DEVICE_ID" ]; then
echo "No iPhone simulator found. Creating one..."
RUNTIME=$(xcrun simctl list runtimes | grep iOS | tail -1 | awk '{print $NF}')
DEVICE_ID=$(xcrun simctl create "iPhone 15 Pro" "iPhone 15 Pro" "$RUNTIME")
fi
# Boot the device
echo "Booting device $DEVICE_ID..."
xcrun simctl boot "$DEVICE_ID" 2>/dev/null || true
# Open Simulator app
open -a Simulator
# Wait for boot (max 30 seconds)
for i in {1..30}; do
if xcrun simctl list devices booted | grep -q "Booted"; then
echo "Simulator booted successfully"
break
fi
sleep 1
done
# Final check
if ! xcrun simctl list devices booted | grep -q "Booted"; then
echo "Failed to boot simulator after 30 seconds"
exit 1
fi
fi
preconditions:
- sh: command -v xcrun
msg: "xcrun not found. Please run 'wails3 task ios:install:deps' to install iOS development dependencies"
generate:ios:overlay:
internal: true
summary: Generate Go build overlay and iOS shim
sources:
- build/config.yml
generates:
- build/ios/xcode/overlay.json
- build/ios/xcode/gen/main_ios.gen.go
cmds:
- wails3 ios overlay:gen -out build/ios/xcode/overlay.json -config build/config.yml
generate:ios:xcode:
internal: true
summary: Generate iOS Xcode project structure and assets
sources:
- build/config.yml
- build/appicon.png
generates:
- build/ios/xcode/main/main.m
- build/ios/xcode/main/Assets.xcassets/**/*
- build/ios/xcode/project.pbxproj
cmds:
- wails3 ios xcode:gen -outdir build/ios/xcode -config build/config.yml
run:
summary: Run the application in iOS Simulator
deps:
- task: ensure-simulator
- task: compile:ios
cmds:
- rm -rf "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- cp "{{.BIN_DIR}}/{{.APP_NAME | lower}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/{{.APP_NAME | lower}}"
- cp build/ios/Info.dev.plist "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Info.plist"
- |
# Compile asset catalog and embed icons for dev bundle
APP_BUNDLE="{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
AC_IN="build/ios/xcode/main/Assets.xcassets"
if [ -d "$AC_IN" ]; then
TMP_AC=$(mktemp -d)
xcrun actool \
--compile "$TMP_AC" \
--app-icon AppIcon \
--platform iphonesimulator \
--minimum-deployment-target 15.0 \
--product-type com.apple.product-type.application \
--target-device iphone \
--target-device ipad \
--output-partial-info-plist "$APP_BUNDLE/assetcatalog_generated_info.plist" \
"$AC_IN"
if [ -f "$TMP_AC/Assets.car" ]; then
cp -f "$TMP_AC/Assets.car" "$APP_BUNDLE/Assets.car"
fi
rm -rf "$TMP_AC"
if [ -f "$APP_BUNDLE/assetcatalog_generated_info.plist" ]; then
/usr/libexec/PlistBuddy -c "Merge $APP_BUNDLE/assetcatalog_generated_info.plist" "$APP_BUNDLE/Info.plist" || true
fi
fi
- codesign --force --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- xcrun simctl terminate booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true
- xcrun simctl uninstall booted "com.wails.{{.APP_NAME | lower}}.dev" 2>/dev/null || true
- xcrun simctl install booted "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- xcrun simctl launch booted "com.wails.{{.APP_NAME | lower}}.dev"
xcode:
summary: Open the generated Xcode project for this app
cmds:
- task: generate:ios:xcode
- open build/ios/xcode/main.xcodeproj
logs:
summary: Stream iOS Simulator logs filtered to this app
cmds:
- |
xcrun simctl spawn booted log stream \
--level debug \
--style compact \
--predicate 'senderImagePath CONTAINS[c] "{{.APP_NAME | lower}}.app/" OR composedMessage CONTAINS[c] "{{.APP_NAME | lower}}" OR eventMessage CONTAINS[c] "{{.APP_NAME | lower}}" OR process == "{{.APP_NAME | lower}}" OR category CONTAINS[c] "{{.APP_NAME | lower}}"'
logs:dev:
summary: Stream logs for the dev bundle (used by `task ios:run`)
cmds:
- |
xcrun simctl spawn booted log stream \
--level debug \
--style compact \
--predicate 'senderImagePath CONTAINS[c] ".dev.app/" OR subsystem == "com.wails.{{.APP_NAME | lower}}.dev" OR process == "{{.APP_NAME | lower}}"'
logs:wide:
summary: Wide log stream to help discover the exact process/bundle identifiers
cmds:
- |
xcrun simctl spawn booted log stream \
--level debug \
--style compact \
--predicate 'senderImagePath CONTAINS[c] ".app/"'

View file

@ -0,0 +1,10 @@
//go:build !ios
package main
import "github.com/wailsapp/wails/v3/pkg/application"
// modifyOptionsForIOS is a no-op on non-iOS platforms
func modifyOptionsForIOS(opts *application.Options) {
// No modifications needed for non-iOS platforms
}

View file

@ -0,0 +1,11 @@
//go:build ios
package main
import "github.com/wailsapp/wails/v3/pkg/application"
// modifyOptionsForIOS adjusts the application options for iOS
func modifyOptionsForIOS(opts *application.Options) {
// Disable signal handlers on iOS to prevent crashes
opts.DisableDefaultSignalHandler = true
}

72
build/ios/build.sh Normal file
View file

@ -0,0 +1,72 @@
#!/bin/bash
set -e
# Build configuration
APP_NAME="core-ide-test"
BUNDLE_ID="com.example.coreidetest"
VERSION="0.1.0"
BUILD_NUMBER="0.1.0"
BUILD_DIR="build/ios"
TARGET="simulator"
echo "Building iOS app: $APP_NAME"
echo "Bundle ID: $BUNDLE_ID"
echo "Version: $VERSION ($BUILD_NUMBER)"
echo "Target: $TARGET"
# Ensure build directory exists
mkdir -p "$BUILD_DIR"
# Determine SDK and target architecture
if [ "$TARGET" = "simulator" ]; then
SDK="iphonesimulator"
ARCH="arm64-apple-ios15.0-simulator"
elif [ "$TARGET" = "device" ]; then
SDK="iphoneos"
ARCH="arm64-apple-ios15.0"
else
echo "Unknown target: $TARGET"
exit 1
fi
# Get SDK path
SDK_PATH=$(xcrun --sdk $SDK --show-sdk-path)
# Compile the application
echo "Compiling with SDK: $SDK"
xcrun -sdk $SDK clang \
-target $ARCH \
-isysroot "$SDK_PATH" \
-framework Foundation \
-framework UIKit \
-framework WebKit \
-framework CoreGraphics \
-o "$BUILD_DIR/$APP_NAME" \
"$BUILD_DIR/main.m"
# Create app bundle
echo "Creating app bundle..."
APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"
rm -rf "$APP_BUNDLE"
mkdir -p "$APP_BUNDLE"
# Move executable
mv "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/"
# Copy Info.plist
cp "$BUILD_DIR/Info.plist" "$APP_BUNDLE/"
# Sign the app
echo "Signing app..."
codesign --force --sign - "$APP_BUNDLE"
echo "Build complete: $APP_BUNDLE"
# Deploy to simulator if requested
if [ "$TARGET" = "simulator" ]; then
echo "Deploying to simulator..."
xcrun simctl terminate booted "$BUNDLE_ID" 2>/dev/null || true
xcrun simctl install booted "$APP_BUNDLE"
xcrun simctl launch booted "$BUNDLE_ID"
echo "App launched on simulator"
fi

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Development entitlements -->
<key>get-task-allow</key>
<true/>
<!-- App Sandbox -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Network access -->
<key>com.apple.security.network.client</key>
<true/>
<!-- File access (read-only) -->
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

3
build/ios/icon.png Normal file
View file

@ -0,0 +1,3 @@
# iOS Icon Placeholder
# This file should be replaced with the actual app icon (1024x1024 PNG)
# The build process will generate all required icon sizes from this base icon

23
build/ios/main.m Normal file
View file

@ -0,0 +1,23 @@
//go:build ios
// Minimal bootstrap: delegate comes from Go archive (WailsAppDelegate)
#import <UIKit/UIKit.h>
#include <stdio.h>
// External Go initialization function from the c-archive (declare before use)
extern void WailsIOSMain();
int main(int argc, char * argv[]) {
@autoreleasepool {
// Disable buffering so stdout/stderr from Go log.Printf flush immediately
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
// Start Go runtime on a background queue to avoid blocking main thread/UI
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
WailsIOSMain();
});
// Run UIApplicationMain using WailsAppDelegate provided by the Go archive
return UIApplicationMain(argc, argv, nil, @"WailsAppDelegate");
}
}

24
build/ios/main_ios.go Normal file
View file

@ -0,0 +1,24 @@
//go:build ios
package main
import (
"C"
)
// For iOS builds, we need to export a function that can be called from Objective-C
// This wrapper allows us to keep the original main.go unmodified
//export WailsIOSMain
func WailsIOSMain() {
// DO NOT lock the goroutine to the current OS thread on iOS!
// This causes signal handling issues:
// "signal 16 received on thread with no signal stack"
// "fatal error: non-Go code disabled sigaltstack"
// iOS apps run in a sandboxed environment where the Go runtime's
// signal handling doesn't work the same way as desktop platforms.
// Call the actual main function from main.go
// This ensures all the user's code is executed
main()
}

222
build/ios/project.pbxproj Normal file
View file

@ -0,0 +1,222 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
C0DEBEEF0000000000000001 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000002 /* main.m */; };
C0DEBEEF00000000000000F1 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000101 /* UIKit.framework */; };
C0DEBEEF00000000000000F2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000102 /* Foundation.framework */; };
C0DEBEEF00000000000000F3 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000103 /* WebKit.framework */; };
C0DEBEEF00000000000000F4 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000104 /* Security.framework */; };
C0DEBEEF00000000000000F5 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000105 /* CoreFoundation.framework */; };
C0DEBEEF00000000000000F6 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000106 /* libresolv.tbd */; };
C0DEBEEF00000000000000F7 /* My Product.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DEBEEF0000000000000107 /* My Product.a */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
C0DEBEEF0000000000000002 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
C0DEBEEF0000000000000003 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C0DEBEEF0000000000000004 /* My Product.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "My Product.app"; sourceTree = BUILT_PRODUCTS_DIR; };
C0DEBEEF0000000000000101 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
C0DEBEEF0000000000000102 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
C0DEBEEF0000000000000103 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
C0DEBEEF0000000000000104 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
C0DEBEEF0000000000000105 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
C0DEBEEF0000000000000106 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.text-based-dylib-definition; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
C0DEBEEF0000000000000107 /* My Product.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "My Product.a"; path = ../../../bin/My Product.a; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
C0DEBEEF0000000000000010 = {
isa = PBXGroup;
children = (
C0DEBEEF0000000000000020 /* Products */,
C0DEBEEF0000000000000045 /* Frameworks */,
C0DEBEEF0000000000000030 /* main */,
);
sourceTree = "<group>";
};
C0DEBEEF0000000000000020 /* Products */ = {
isa = PBXGroup;
children = (
C0DEBEEF0000000000000004 /* My Product.app */,
);
name = Products;
sourceTree = "<group>";
};
C0DEBEEF0000000000000030 /* main */ = {
isa = PBXGroup;
children = (
C0DEBEEF0000000000000002 /* main.m */,
C0DEBEEF0000000000000003 /* Info.plist */,
);
path = main;
sourceTree = SOURCE_ROOT;
};
C0DEBEEF0000000000000045 /* Frameworks */ = {
isa = PBXGroup;
children = (
C0DEBEEF0000000000000101 /* UIKit.framework */,
C0DEBEEF0000000000000102 /* Foundation.framework */,
C0DEBEEF0000000000000103 /* WebKit.framework */,
C0DEBEEF0000000000000104 /* Security.framework */,
C0DEBEEF0000000000000105 /* CoreFoundation.framework */,
C0DEBEEF0000000000000106 /* libresolv.tbd */,
C0DEBEEF0000000000000107 /* My Product.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
C0DEBEEF0000000000000040 /* My Product */ = {
isa = PBXNativeTarget;
buildConfigurationList = C0DEBEEF0000000000000070 /* Build configuration list for PBXNativeTarget "My Product" */;
buildPhases = (
C0DEBEEF0000000000000055 /* Prebuild: Wails Go Archive */,
C0DEBEEF0000000000000050 /* Sources */,
C0DEBEEF0000000000000056 /* Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "My Product";
productName = "My Product";
productReference = C0DEBEEF0000000000000004 /* My Product.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
C0DEBEEF0000000000000060 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1500;
ORGANIZATIONNAME = "My Company";
TargetAttributes = {
C0DEBEEF0000000000000040 = {
CreatedOnToolsVersion = 15.0;
};
};
};
buildConfigurationList = C0DEBEEF0000000000000080 /* Build configuration list for PBXProject "main" */;
compatibilityVersion = "Xcode 15.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = C0DEBEEF0000000000000010;
productRefGroup = C0DEBEEF0000000000000020 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
C0DEBEEF0000000000000040 /* My Product */,
);
};
/* End PBXProject section */
/* Begin PBXFrameworksBuildPhase section */
C0DEBEEF0000000000000056 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C0DEBEEF00000000000000F7 /* My Product.a in Frameworks */,
C0DEBEEF00000000000000F1 /* UIKit.framework in Frameworks */,
C0DEBEEF00000000000000F2 /* Foundation.framework in Frameworks */,
C0DEBEEF00000000000000F3 /* WebKit.framework in Frameworks */,
C0DEBEEF00000000000000F4 /* Security.framework in Frameworks */,
C0DEBEEF00000000000000F5 /* CoreFoundation.framework in Frameworks */,
C0DEBEEF00000000000000F6 /* libresolv.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
C0DEBEEF0000000000000055 /* Prebuild: Wails Go Archive */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Prebuild: Wails Go Archive";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "set -e\nAPP_ROOT=\"${PROJECT_DIR}/../../..\"\nSDK_PATH=$(xcrun --sdk iphonesimulator --show-sdk-path)\nexport GOOS=ios\nexport GOARCH=arm64\nexport CGO_ENABLED=1\nexport CGO_CFLAGS=\"-isysroot ${SDK_PATH} -target arm64-apple-ios15.0-simulator -mios-simulator-version-min=15.0\"\nexport CGO_LDFLAGS=\"-isysroot ${SDK_PATH} -target arm64-apple-ios15.0-simulator\"\ncd \"${APP_ROOT}\"\n# Ensure overlay exists\nif [ ! -f build/ios/xcode/overlay.json ]; then\n wails3 ios overlay:gen -out build/ios/xcode/overlay.json -config build/config.yml || true\nfi\n# Build Go c-archive if missing or older than sources\nif [ ! -f bin/My Product.a ]; then\n echo \"Building Go c-archive...\"\n go build -buildmode=c-archive -overlay build/ios/xcode/overlay.json -o bin/My Product.a\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C0DEBEEF0000000000000050 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C0DEBEEF0000000000000001 /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
C0DEBEEF0000000000000090 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
INFOPLIST_FILE = main/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.example.coreidetest";
PRODUCT_NAME = "My Product";
CODE_SIGNING_ALLOWED = NO;
SDKROOT = iphonesimulator;
};
name = Debug;
};
C0DEBEEF00000000000000A0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
INFOPLIST_FILE = main/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.example.coreidetest";
PRODUCT_NAME = "My Product";
CODE_SIGNING_ALLOWED = NO;
SDKROOT = iphonesimulator;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
C0DEBEEF0000000000000070 /* Build configuration list for PBXNativeTarget "My Product" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C0DEBEEF0000000000000090 /* Debug */,
C0DEBEEF00000000000000A0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
C0DEBEEF0000000000000080 /* Build configuration list for PBXProject "main" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C0DEBEEF0000000000000090 /* Debug */,
C0DEBEEF00000000000000A0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
};
rootObject = C0DEBEEF0000000000000060 /* Project object */;
}

View file

@ -0,0 +1,319 @@
// install_deps.go - iOS development dependency checker
// This script checks for required iOS development tools.
// It's designed to be portable across different shells by using Go instead of shell scripts.
//
// Usage:
// go run install_deps.go # Interactive mode
// TASK_FORCE_YES=true go run install_deps.go # Auto-accept prompts
// CI=true go run install_deps.go # CI mode (auto-accept)
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"
)
type Dependency struct {
Name string
CheckFunc func() (bool, string) // Returns (success, details)
Required bool
InstallCmd []string
InstallMsg string
SuccessMsg string
FailureMsg string
}
func main() {
fmt.Println("Checking iOS development dependencies...")
fmt.Println("=" + strings.Repeat("=", 50))
fmt.Println()
hasErrors := false
dependencies := []Dependency{
{
Name: "Xcode",
CheckFunc: func() (bool, string) {
// Check if xcodebuild exists
if !checkCommand([]string{"xcodebuild", "-version"}) {
return false, ""
}
// Get version info
out, err := exec.Command("xcodebuild", "-version").Output()
if err != nil {
return false, ""
}
lines := strings.Split(string(out), "\n")
if len(lines) > 0 {
return true, strings.TrimSpace(lines[0])
}
return true, ""
},
Required: true,
InstallMsg: "Please install Xcode from the Mac App Store:\n https://apps.apple.com/app/xcode/id497799835\n Xcode is REQUIRED for iOS development (includes iOS SDKs, simulators, and frameworks)",
SuccessMsg: "✅ Xcode found",
FailureMsg: "❌ Xcode not found (REQUIRED)",
},
{
Name: "Xcode Developer Path",
CheckFunc: func() (bool, string) {
// Check if xcode-select points to a valid Xcode path
out, err := exec.Command("xcode-select", "-p").Output()
if err != nil {
return false, "xcode-select not configured"
}
path := strings.TrimSpace(string(out))
// Check if path exists and is in Xcode.app
if _, err := os.Stat(path); err != nil {
return false, "Invalid Xcode path"
}
// Verify it's pointing to Xcode.app (not just Command Line Tools)
if !strings.Contains(path, "Xcode.app") {
return false, fmt.Sprintf("Points to %s (should be Xcode.app)", path)
}
return true, path
},
Required: true,
InstallCmd: []string{"sudo", "xcode-select", "-s", "/Applications/Xcode.app/Contents/Developer"},
InstallMsg: "Xcode developer path needs to be configured",
SuccessMsg: "✅ Xcode developer path configured",
FailureMsg: "❌ Xcode developer path not configured correctly",
},
{
Name: "iOS SDK",
CheckFunc: func() (bool, string) {
// Get the iOS Simulator SDK path
cmd := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path")
output, err := cmd.Output()
if err != nil {
return false, "Cannot find iOS SDK"
}
sdkPath := strings.TrimSpace(string(output))
// Check if the SDK path exists
if _, err := os.Stat(sdkPath); err != nil {
return false, "iOS SDK path not found"
}
// Check for UIKit framework (essential for iOS development)
uikitPath := fmt.Sprintf("%s/System/Library/Frameworks/UIKit.framework", sdkPath)
if _, err := os.Stat(uikitPath); err != nil {
return false, "UIKit.framework not found"
}
// Get SDK version
versionCmd := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-version")
versionOut, _ := versionCmd.Output()
version := strings.TrimSpace(string(versionOut))
return true, fmt.Sprintf("iOS %s SDK", version)
},
Required: true,
InstallMsg: "iOS SDK comes with Xcode. Please ensure Xcode is properly installed.",
SuccessMsg: "✅ iOS SDK found with UIKit framework",
FailureMsg: "❌ iOS SDK not found or incomplete",
},
{
Name: "iOS Simulator Runtime",
CheckFunc: func() (bool, string) {
if !checkCommand([]string{"xcrun", "simctl", "help"}) {
return false, ""
}
// Check if we can list runtimes
out, err := exec.Command("xcrun", "simctl", "list", "runtimes").Output()
if err != nil {
return false, "Cannot access simulator"
}
// Count iOS runtimes
lines := strings.Split(string(out), "\n")
count := 0
var versions []string
for _, line := range lines {
if strings.Contains(line, "iOS") && !strings.Contains(line, "unavailable") {
count++
// Extract version number
if parts := strings.Fields(line); len(parts) > 2 {
for _, part := range parts {
if strings.HasPrefix(part, "(") && strings.HasSuffix(part, ")") {
versions = append(versions, strings.Trim(part, "()"))
break
}
}
}
}
}
if count > 0 {
return true, fmt.Sprintf("%d runtime(s): %s", count, strings.Join(versions, ", "))
}
return false, "No iOS runtimes installed"
},
Required: true,
InstallMsg: "iOS Simulator runtimes come with Xcode. You may need to download them:\n Xcode → Settings → Platforms → iOS",
SuccessMsg: "✅ iOS Simulator runtime available",
FailureMsg: "❌ iOS Simulator runtime not available",
},
}
// Check each dependency
for _, dep := range dependencies {
success, details := dep.CheckFunc()
if success {
msg := dep.SuccessMsg
if details != "" {
msg = fmt.Sprintf("%s (%s)", dep.SuccessMsg, details)
}
fmt.Println(msg)
} else {
fmt.Println(dep.FailureMsg)
if details != "" {
fmt.Printf(" Details: %s\n", details)
}
if dep.Required {
hasErrors = true
if len(dep.InstallCmd) > 0 {
fmt.Println()
fmt.Println(" " + dep.InstallMsg)
fmt.Printf(" Fix command: %s\n", strings.Join(dep.InstallCmd, " "))
if promptUser("Do you want to run this command?") {
fmt.Println("Running command...")
cmd := exec.Command(dep.InstallCmd[0], dep.InstallCmd[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
fmt.Printf("Command failed: %v\n", err)
os.Exit(1)
}
fmt.Println("✅ Command completed. Please run this check again.")
} else {
fmt.Printf(" Please run manually: %s\n", strings.Join(dep.InstallCmd, " "))
}
} else {
fmt.Println(" " + dep.InstallMsg)
}
}
}
}
// Check for iPhone simulators
fmt.Println()
fmt.Println("Checking for iPhone simulator devices...")
if !checkCommand([]string{"xcrun", "simctl", "list", "devices"}) {
fmt.Println("❌ Cannot check for iPhone simulators")
hasErrors = true
} else {
out, err := exec.Command("xcrun", "simctl", "list", "devices").Output()
if err != nil {
fmt.Println("❌ Failed to list simulator devices")
hasErrors = true
} else if !strings.Contains(string(out), "iPhone") {
fmt.Println("⚠️ No iPhone simulator devices found")
fmt.Println()
// Get the latest iOS runtime
runtimeOut, err := exec.Command("xcrun", "simctl", "list", "runtimes").Output()
if err != nil {
fmt.Println(" Failed to get iOS runtimes:", err)
} else {
lines := strings.Split(string(runtimeOut), "\n")
var latestRuntime string
for _, line := range lines {
if strings.Contains(line, "iOS") && !strings.Contains(line, "unavailable") {
// Extract runtime identifier
parts := strings.Fields(line)
if len(parts) > 0 {
latestRuntime = parts[len(parts)-1]
}
}
}
if latestRuntime == "" {
fmt.Println(" No iOS runtime found. Please install iOS simulators in Xcode:")
fmt.Println(" Xcode → Settings → Platforms → iOS")
} else {
fmt.Println(" Would you like to create an iPhone 15 Pro simulator?")
createCmd := []string{"xcrun", "simctl", "create", "iPhone 15 Pro", "iPhone 15 Pro", latestRuntime}
fmt.Printf(" Command: %s\n", strings.Join(createCmd, " "))
if promptUser("Create simulator?") {
cmd := exec.Command(createCmd[0], createCmd[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf(" Failed to create simulator: %v\n", err)
} else {
fmt.Println(" ✅ iPhone 15 Pro simulator created")
}
} else {
fmt.Println(" Skipping simulator creation")
fmt.Printf(" Create manually: %s\n", strings.Join(createCmd, " "))
}
}
}
} else {
// Count iPhone devices
count := 0
lines := strings.Split(string(out), "\n")
for _, line := range lines {
if strings.Contains(line, "iPhone") && !strings.Contains(line, "unavailable") {
count++
}
}
fmt.Printf("✅ %d iPhone simulator device(s) available\n", count)
}
}
// Final summary
fmt.Println()
fmt.Println("=" + strings.Repeat("=", 50))
if hasErrors {
fmt.Println("❌ Some required dependencies are missing or misconfigured.")
fmt.Println()
fmt.Println("Quick setup guide:")
fmt.Println("1. Install Xcode from Mac App Store (if not installed)")
fmt.Println("2. Open Xcode once and agree to the license")
fmt.Println("3. Install additional components when prompted")
fmt.Println("4. Run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer")
fmt.Println("5. Download iOS simulators: Xcode → Settings → Platforms → iOS")
fmt.Println("6. Run this check again")
os.Exit(1)
} else {
fmt.Println("✅ All required dependencies are installed!")
fmt.Println(" You're ready for iOS development with Wails!")
}
}
func checkCommand(args []string) bool {
if len(args) == 0 {
return false
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = nil
cmd.Stderr = nil
err := cmd.Run()
return err == nil
}
func promptUser(question string) bool {
// Check if we're in a non-interactive environment
if os.Getenv("CI") != "" || os.Getenv("TASK_FORCE_YES") == "true" {
fmt.Printf("%s [y/N]: y (auto-accepted)\n", question)
return true
}
reader := bufio.NewReader(os.Stdin)
fmt.Printf("%s [y/N]: ", question)
response, err := reader.ReadString('\n')
if err != nil {
return false
}
response = strings.ToLower(strings.TrimSpace(response))
return response == "y" || response == "yes"
}

View file

@ -3,34 +3,108 @@ version: '3'
includes: includes:
common: ../Taskfile.yml common: ../Taskfile.yml
vars:
# Signing configuration - edit these values for your project
# PGP_KEY: "path/to/signing-key.asc"
# SIGN_ROLE: "builder" # Options: origin, maint, archive, builder
#
# Password is stored securely in system keychain. Run: wails3 setup signing
# Docker image for cross-compilation (used when building on non-Linux or no CC available)
CROSS_IMAGE: wails-cross
tasks: tasks:
build: build:
summary: Builds the application for Linux summary: Builds the application for Linux
cmds:
# Linux requires CGO - use Docker when:
# 1. Cross-compiling from non-Linux, OR
# 2. No C compiler is available, OR
# 3. Target architecture differs from host architecture (cross-arch compilation)
- task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}'
vars:
ARCH: '{{.ARCH}}'
DEV: '{{.DEV}}'
OUTPUT: '{{.OUTPUT}}'
EXTRA_TAGS: '{{.EXTRA_TAGS}}'
vars:
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Determine target architecture (defaults to host ARCH if not specified)
TARGET_ARCH: '{{.ARCH | default ARCH}}'
# Check if a C compiler is available (gcc or clang)
HAS_CC:
sh: '(command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1) && echo "true" || echo "false"'
build:native:
summary: Builds the application natively on Linux
internal: true
deps: deps:
- task: common:go:mod:tidy - task: common:go:mod:tidy
- task: common:build:frontend - task: common:build:frontend
vars: vars:
BUILD_FLAGS: BUILD_FLAGS:
ref: .BUILD_FLAGS ref: .BUILD_FLAGS
PRODUCTION: DEV:
ref: .PRODUCTION ref: .DEV
- task: common:generate:icons - task: common:generate:icons
- task: generate:dotdesktop
cmds: cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
vars: vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
env: env:
GOOS: linux GOOS: linux
CGO_ENABLED: 1 CGO_ENABLED: 1
GOARCH: '{{.ARCH | default ARCH}}' GOARCH: '{{.ARCH | default ARCH}}'
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:docker:
summary: Builds for Linux using Docker (for non-Linux hosts or when no C compiler available)
internal: true
deps:
- task: common:build:frontend
- task: common:generate:icons
- task: generate:dotdesktop
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required for cross-compilation to Linux. Please install Docker."
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
msg: |
Docker image '{{.CROSS_IMAGE}}' not found.
Build it first: wails3 task setup:docker
cmds:
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
- mkdir -p {{.BIN_DIR}}
- mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
vars:
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Mount Go module cache for faster builds
GO_CACHE_MOUNT:
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
# Extract replace directives from go.mod and create -v mounts for each
REPLACE_MOUNTS:
sh: |
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
# Convert relative paths to absolute
if [ "${path#/}" = "$path" ]; then
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
fi
# Only mount if directory exists
if [ -d "$path" ]; then
echo "-v $path:$path:ro"
fi
done | tr '\n' ' '
package: package:
summary: Packages a production build of the application for Linux summary: Packages the application for Linux
deps: deps:
- task: build - task: build
vars:
PRODUCTION: "true"
cmds: cmds:
- task: create:appimage - task: create:appimage
- task: create:deb - task: create:deb
@ -42,13 +116,11 @@ tasks:
dir: build/linux/appimage dir: build/linux/appimage
deps: deps:
- task: build - task: build
vars:
PRODUCTION: "true"
- task: generate:dotdesktop - task: generate:dotdesktop
cmds: cmds:
- cp {{.APP_BINARY}} {{.APP_NAME}} - cp "{{.APP_BINARY}}" "{{.APP_NAME}}"
- cp ../../appicon.png {{.APP_NAME}}.png - cp ../../appicon.png "{{.APP_NAME}}.png"
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build - wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
vars: vars:
APP_NAME: '{{.APP_NAME}}' APP_NAME: '{{.APP_NAME}}'
APP_BINARY: '../../../bin/{{.APP_NAME}}' APP_BINARY: '../../../bin/{{.APP_NAME}}'
@ -60,8 +132,6 @@ tasks:
summary: Creates a deb package summary: Creates a deb package
deps: deps:
- task: build - task: build
vars:
PRODUCTION: "true"
cmds: cmds:
- task: generate:dotdesktop - task: generate:dotdesktop
- task: generate:deb - task: generate:deb
@ -70,8 +140,6 @@ tasks:
summary: Creates a rpm package summary: Creates a rpm package
deps: deps:
- task: build - task: build
vars:
PRODUCTION: "true"
cmds: cmds:
- task: generate:dotdesktop - task: generate:dotdesktop
- task: generate:rpm - task: generate:rpm
@ -80,8 +148,6 @@ tasks:
summary: Creates a arch linux packager package summary: Creates a arch linux packager package
deps: deps:
- task: build - task: build
vars:
PRODUCTION: "true"
cmds: cmds:
- task: generate:dotdesktop - task: generate:dotdesktop
- task: generate:aur - task: generate:aur
@ -89,24 +155,24 @@ tasks:
generate:deb: generate:deb:
summary: Creates a deb package summary: Creates a deb package
cmds: cmds:
- wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin - wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:rpm: generate:rpm:
summary: Creates a rpm package summary: Creates a rpm package
cmds: cmds:
- wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin - wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:aur: generate:aur:
summary: Creates a arch linux packager package summary: Creates a arch linux packager package
cmds: cmds:
- wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin - wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:dotdesktop: generate:dotdesktop:
summary: Generates a `.desktop` file summary: Generates a `.desktop` file
dir: build dir: build
cmds: cmds:
- mkdir -p {{.ROOT_DIR}}/build/linux/appimage - mkdir -p {{.ROOT_DIR}}/build/linux/appimage
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}" - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}"
vars: vars:
APP_NAME: '{{.APP_NAME}}' APP_NAME: '{{.APP_NAME}}'
EXEC: '{{.APP_NAME}}' EXEC: '{{.APP_NAME}}'
@ -117,3 +183,44 @@ tasks:
run: run:
cmds: cmds:
- '{{.BIN_DIR}}/{{.APP_NAME}}' - '{{.BIN_DIR}}/{{.APP_NAME}}'
sign:deb:
summary: Signs the DEB package
desc: |
Signs the .deb package with a PGP key.
Configure PGP_KEY in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:deb
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}}
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
sign:rpm:
summary: Signs the RPM package
desc: |
Signs the .rpm package with a PGP key.
Configure PGP_KEY in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:rpm
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" --pgp-key {{.PGP_KEY}}
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
sign:packages:
summary: Signs all Linux packages (DEB and RPM)
desc: |
Signs both .deb and .rpm packages with a PGP key.
Configure PGP_KEY in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
cmds:
- task: sign:deb
- task: sign:rpm
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"

View file

@ -3,42 +3,99 @@ version: '3'
includes: includes:
common: ../Taskfile.yml common: ../Taskfile.yml
vars:
# Signing configuration - edit these values for your project
# SIGN_CERTIFICATE: "path/to/certificate.pfx"
# SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE
# TIMESTAMP_SERVER: "http://timestamp.digicert.com"
#
# Password is stored securely in system keychain. Run: wails3 setup signing
# Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows)
CROSS_IMAGE: wails-cross
tasks: tasks:
build: build:
summary: Builds the application for Windows summary: Builds the application for Windows
cmds:
# Auto-detect CGO: if CGO_ENABLED=1, use Docker; otherwise use native Go cross-compile
- task: '{{if and (ne OS "windows") (eq .CGO_ENABLED "1")}}build:docker{{else}}build:native{{end}}'
vars:
ARCH: '{{.ARCH}}'
DEV: '{{.DEV}}'
EXTRA_TAGS: '{{.EXTRA_TAGS}}'
vars:
# Default to CGO_ENABLED=0 if not explicitly set
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
build:native:
summary: Builds the application using native Go cross-compilation
internal: true
deps: deps:
- task: common:go:mod:tidy - task: common:go:mod:tidy
- task: common:build:frontend - task: common:build:frontend
vars: vars:
BUILD_FLAGS: BUILD_FLAGS:
ref: .BUILD_FLAGS ref: .BUILD_FLAGS
PRODUCTION: DEV:
ref: .PRODUCTION ref: .DEV
- task: common:generate:icons - task: common:generate:icons
cmds: cmds:
- task: generate:syso - task: generate:syso
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe - go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe"
- cmd: powershell Remove-item *.syso - cmd: powershell Remove-item *.syso
platforms: [windows] platforms: [windows]
- cmd: rm -f *.syso - cmd: rm -f *.syso
platforms: [linux, darwin] platforms: [linux, darwin]
vars: vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}'
env: env:
GOOS: windows GOOS: windows
CGO_ENABLED: 0 CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
GOARCH: '{{.ARCH | default ARCH}}' GOARCH: '{{.ARCH | default ARCH}}'
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:docker:
summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows)
internal: true
deps:
- task: common:build:frontend
- task: common:generate:icons
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required for CGO cross-compilation. Please install Docker."
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
msg: |
Docker image '{{.CROSS_IMAGE}}' not found.
Build it first: wails3 task setup:docker
cmds:
- task: generate:syso
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
- rm -f *.syso
vars:
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
# Mount Go module cache for faster builds
GO_CACHE_MOUNT:
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
# Extract replace directives from go.mod and create -v mounts for each
REPLACE_MOUNTS:
sh: |
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
# Convert relative paths to absolute
if [ "${path#/}" = "$path" ]; then
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
fi
# Only mount if directory exists
if [ -d "$path" ]; then
echo "-v $path:$path:ro"
fi
done | tr '\n' ' '
package: package:
summary: Packages a production build of the application summary: Packages the application
cmds: cmds:
- |- - task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}'
if [ "{{.FORMAT | default "nsis"}}" = "msix" ]; then
task create:msix:package
else
task create:nsis:installer
fi
vars: vars:
FORMAT: '{{.FORMAT | default "nsis"}}' FORMAT: '{{.FORMAT | default "nsis"}}'
@ -55,12 +112,15 @@ tasks:
dir: build/windows/nsis dir: build/windows/nsis
deps: deps:
- task: build - task: build
vars:
PRODUCTION: "true"
cmds: cmds:
# Create the Microsoft WebView2 bootstrapper if it doesn't exist # Create the Microsoft WebView2 bootstrapper if it doesn't exist
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
- makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi - |
{{if eq OS "windows"}}
makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
{{else}}
makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi
{{end}}
vars: vars:
ARCH: '{{.ARCH | default ARCH}}' ARCH: '{{.ARCH | default ARCH}}'
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
@ -69,8 +129,6 @@ tasks:
summary: Creates an MSIX package summary: Creates an MSIX package
deps: deps:
- task: build - task: build
vars:
PRODUCTION: "true"
cmds: cmds:
- |- - |-
wails3 tool msix \ wails3 tool msix \
@ -96,3 +154,31 @@ tasks:
run: run:
cmds: cmds:
- '{{.BIN_DIR}}/{{.APP_NAME}}.exe' - '{{.BIN_DIR}}/{{.APP_NAME}}.exe'
sign:
summary: Signs the Windows executable
desc: |
Signs the .exe with an Authenticode certificate.
Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: build
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"
sign:installer:
summary: Signs the NSIS installer
desc: |
Creates and signs the NSIS installer.
Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:nsis:installer
cmds:
- wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"

File diff suppressed because it is too large Load diff

View file

@ -25,13 +25,13 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/common": "^20.3.14", "@angular/common": "~20.3.16",
"@angular/compiler": "^20.3.16", "@angular/compiler": "~20.3.16",
"@angular/core": "^21.1.2", "@angular/core": "~20.3.16",
"@angular/forms": "^20.3.0", "@angular/forms": "~20.3.16",
"@angular/platform-browser": "^20.3.0", "@angular/platform-browser": "~20.3.16",
"@angular/platform-server": "^20.3.0", "@angular/platform-server": "~20.3.16",
"@angular/router": "^20.3.0", "@angular/router": "~20.3.16",
"@angular/ssr": "^20.3.6", "@angular/ssr": "^20.3.6",
"@wailsio/runtime": "3.0.0-alpha.72", "@wailsio/runtime": "3.0.0-alpha.72",
"express": "^5.1.0", "express": "^5.1.0",
@ -40,9 +40,9 @@
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^21.1.2", "@angular/build": "^20.3.6",
"@angular/cli": "^21.1.3", "@angular/cli": "^20.3.6",
"@angular/compiler-cli": "^20.3.0", "@angular/compiler-cli": "~20.3.16",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/node": "^20.17.19", "@types/node": "^20.17.19",

2
go.mod
View file

@ -6,7 +6,7 @@ require (
forge.lthn.ai/core/api v0.1.0 forge.lthn.ai/core/api v0.1.0
forge.lthn.ai/core/config v0.1.0 forge.lthn.ai/core/config v0.1.0
forge.lthn.ai/core/go v0.2.2 forge.lthn.ai/core/go v0.2.2
forge.lthn.ai/core/go-process v0.1.0 forge.lthn.ai/core/go-process v0.1.2
forge.lthn.ai/core/go-ws v0.1.3 forge.lthn.ai/core/go-ws v0.1.3
forge.lthn.ai/core/gui v0.0.0 forge.lthn.ai/core/gui v0.0.0
forge.lthn.ai/core/mcp v0.0.0 forge.lthn.ai/core/mcp v0.0.0

9
go.sum
View file

@ -1,7 +1,9 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
forge.lthn.ai/core/api v0.1.0 h1:ZKnQx+L9vxLQSEjwpsD1eNcIQrE4YKV1c2AlMtseM6o= forge.lthn.ai/core/api v0.1.0 h1:ZKnQx+L9vxLQSEjwpsD1eNcIQrE4YKV1c2AlMtseM6o=
forge.lthn.ai/core/api v0.1.0/go.mod h1:c86Lk9AmaS0xbiRCEG/+du8s9KyYNHnp8RED35gR/Fo=
forge.lthn.ai/core/config v0.1.0 h1:qj14x/dnOWcsXMBQWAT3FtA+/sy6Qd+1NFTg5Xoil1I= forge.lthn.ai/core/config v0.1.0 h1:qj14x/dnOWcsXMBQWAT3FtA+/sy6Qd+1NFTg5Xoil1I=
forge.lthn.ai/core/config v0.1.0/go.mod h1:8HYA29drAWlX+bO4VI1JhmKUgGU66E2Xge8D3tKd3Dg=
forge.lthn.ai/core/go v0.2.2 h1:JCWaFfiG+agb0f7b5DO1g+h40x6nb4UydxJ7D+oZk5k= forge.lthn.ai/core/go v0.2.2 h1:JCWaFfiG+agb0f7b5DO1g+h40x6nb4UydxJ7D+oZk5k=
forge.lthn.ai/core/go v0.2.2/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc= forge.lthn.ai/core/go v0.2.2/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc=
forge.lthn.ai/core/go-ai v0.1.5 h1:iOKW5Wv4Pquc5beDw0QqaKspq3pUvqXxT8IEdCT13Go= forge.lthn.ai/core/go-ai v0.1.5 h1:iOKW5Wv4Pquc5beDw0QqaKspq3pUvqXxT8IEdCT13Go=
@ -18,8 +20,11 @@ forge.lthn.ai/core/go-mlx v0.1.0 h1:nMDhMma3M9iSm2ymNyqMe+aAbJDasNnxgi/1dZ+Zq7c=
forge.lthn.ai/core/go-mlx v0.1.0/go.mod h1:b4BJX67nx9QZiyREl2lmYIPJ+Yp5amZug3y7vXaRy/Y= forge.lthn.ai/core/go-mlx v0.1.0/go.mod h1:b4BJX67nx9QZiyREl2lmYIPJ+Yp5amZug3y7vXaRy/Y=
forge.lthn.ai/core/go-process v0.1.0 h1:lRpliQuu/Omt+kAHMFoQYOb5PKEIKg8yTMchFhejpK8= forge.lthn.ai/core/go-process v0.1.0 h1:lRpliQuu/Omt+kAHMFoQYOb5PKEIKg8yTMchFhejpK8=
forge.lthn.ai/core/go-process v0.1.0/go.mod h1:GGEDrgDLBlj55BkyyZTQpgbcEkAttJznxbUDBRxaAww= forge.lthn.ai/core/go-process v0.1.0/go.mod h1:GGEDrgDLBlj55BkyyZTQpgbcEkAttJznxbUDBRxaAww=
forge.lthn.ai/core/go-process v0.1.2 h1:0fdLJq/DPssilN9E5yude/xHNfZRKHghIjo++b5aXgc=
forge.lthn.ai/core/go-process v0.1.2/go.mod h1:9oxVALrZaZCqFe8YDdheIS5bRUV1SBz4tVW/MflAtxM=
forge.lthn.ai/core/go-rag v0.1.0 h1:H5umiRryuq6J6l889s0OsxWpmq5P5c3A9Bkj0cQyO7k= forge.lthn.ai/core/go-rag v0.1.0 h1:H5umiRryuq6J6l889s0OsxWpmq5P5c3A9Bkj0cQyO7k=
forge.lthn.ai/core/go-rag v0.1.0/go.mod h1:bB8Fy98G2zxVoe7k2B85gXvim6frJdbAMnDyW4peUVU= forge.lthn.ai/core/go-rag v0.1.0/go.mod h1:bB8Fy98G2zxVoe7k2B85gXvim6frJdbAMnDyW4peUVU=
forge.lthn.ai/core/go-scm v0.1.0/go.mod h1:QrSFTqkBS/KgFiNrVngrY8nEwS0u41BjUAu/IEpXiRI=
forge.lthn.ai/core/go-webview v0.1.2 h1:KC5AlkAg1SkmeUVsW4ZeEyrNxy+cZLaHu6SPwWLBi1A= forge.lthn.ai/core/go-webview v0.1.2 h1:KC5AlkAg1SkmeUVsW4ZeEyrNxy+cZLaHu6SPwWLBi1A=
forge.lthn.ai/core/go-webview v0.1.2/go.mod h1:AcIN8cASb7N9c/G5VMmFMIXcPvf6Kk7HcbOQxfiylQ8= forge.lthn.ai/core/go-webview v0.1.2/go.mod h1:AcIN8cASb7N9c/G5VMmFMIXcPvf6Kk7HcbOQxfiylQ8=
forge.lthn.ai/core/go-ws v0.1.3 h1:TzqFpEcDYcZUFFmrTznfEuVcVdnp2jsRNwAGHeTyXN0= forge.lthn.ai/core/go-ws v0.1.3 h1:TzqFpEcDYcZUFFmrTznfEuVcVdnp2jsRNwAGHeTyXN0=
@ -144,6 +149,7 @@ github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8
github.com/gin-contrib/timeout v1.1.0 h1:WAmWseo5gfBUbMrMJu5hJxDclehfSJUmK2wGwCC/EFw= github.com/gin-contrib/timeout v1.1.0 h1:WAmWseo5gfBUbMrMJu5hJxDclehfSJUmK2wGwCC/EFw=
github.com/gin-contrib/timeout v1.1.0/go.mod h1:NpRo4gd1Ad8ZQ4T6bQLVFDqiplCmPRs2nvfckxS2Fw4= github.com/gin-contrib/timeout v1.1.0/go.mod h1:NpRo4gd1Ad8ZQ4T6bQLVFDqiplCmPRs2nvfckxS2Fw4=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@ -396,6 +402,7 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.65.0 h1:LSJsvNqhj2sBNFb5NWHbyDK4QJ/skQ2ydjeOZ9OYNZ4= go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.65.0 h1:LSJsvNqhj2sBNFb5NWHbyDK4QJ/skQ2ydjeOZ9OYNZ4=
@ -441,6 +448,7 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -486,6 +494,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=