mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
ci(release): harden packaged app builds
This commit is contained in:
@@ -59,6 +59,7 @@ jobs:
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libappindicator3-dev \
|
||||
librsvg2-dev \
|
||||
curl \
|
||||
patchelf \
|
||||
libdbus-1-dev \
|
||||
pkg-config \
|
||||
@@ -68,49 +69,46 @@ jobs:
|
||||
- name: Provision locked engines
|
||||
if: runner.os != 'macOS'
|
||||
run: node scripts/provision-engines.js --target ${{ matrix.target }}
|
||||
- name: Hide all engines from linuxdeploy (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
# Patch tauri.conf.json's beforeBuildCommand to empty engine-dist
|
||||
# AFTER stage-engines.js verifies it, but BEFORE tauri builds the bundle.
|
||||
# This hides all static binaries (ffmpeg, aria2c, etc) from linuxdeploy
|
||||
# which corrupts them via patchelf. We will repack them into the AppImage later.
|
||||
sed -i 's/npm run build/rm -rf src-tauri\/engine-dist\/* \&\& npm run build/' src-tauri/tauri.conf.json
|
||||
- name: Build package
|
||||
run: npm run tauri build -- -vv --target ${{ matrix.target }} --bundles ${{ matrix.bundles }}
|
||||
env:
|
||||
APPIMAGE_EXTRACT_AND_RUN: 1
|
||||
FIRELINK_OMIT_ENGINE_DIST_FOR_TAURI_BUNDLE: ${{ runner.os == 'Linux' && '1' || '' }}
|
||||
- name: Install pinned appimagetool (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
APPIMAGETOOL_URL: https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
APPIMAGETOOL_SHA256: a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0
|
||||
run: |
|
||||
curl -fsSL "$APPIMAGETOOL_URL" -o "$RUNNER_TEMP/appimagetool"
|
||||
echo "$APPIMAGETOOL_SHA256 $RUNNER_TEMP/appimagetool" | sha256sum -c -
|
||||
chmod +x "$RUNNER_TEMP/appimagetool"
|
||||
- name: Repack AppImage with engines (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
APPDIR="src-tauri/target/${{ matrix.target }}/release/bundle/appimage/Firelink.AppDir"
|
||||
# Ensure target directory exists in AppDir
|
||||
mkdir -p "$APPDIR/usr/lib/Firelink/engine-dist/${{ matrix.target }}"
|
||||
# Copy everything directly from provisioned-engines into the AppDir
|
||||
cp -r src-tauri/provisioned-engines/${{ matrix.target }}/* "$APPDIR/usr/lib/Firelink/engine-dist/${{ matrix.target }}/"
|
||||
|
||||
# Download appimagetool
|
||||
wget -qO appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool
|
||||
|
||||
# Find the original AppImage name to overwrite
|
||||
APPIMAGE=$(ls src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage | head -n 1)
|
||||
|
||||
# Repack the AppImage
|
||||
env APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 ./appimagetool "$APPDIR" "$APPIMAGE"
|
||||
run: node scripts/repack-linux-appimage-engines.js --target ${{ matrix.target }} --appimagetool "$RUNNER_TEMP/appimagetool"
|
||||
- name: Verify macOS packaged engines and launch
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
APP="src-tauri/target/${{ matrix.target }}/release/bundle/macos/Firelink.app"
|
||||
node scripts/verify-binaries.js --search-root "$APP" --target ${{ matrix.target }}
|
||||
node scripts/smoke-packaged-app.js --executable "$APP/Contents/MacOS/firelink"
|
||||
- name: Verify Windows installer payload
|
||||
- name: Verify Windows installer payload and launch
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$installer = Get-ChildItem "src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe" | Select-Object -First 1
|
||||
7z x $installer.FullName "-o$env:RUNNER_TEMP/firelink-installer" -y
|
||||
node scripts/verify-binaries.js --search-root "$env:RUNNER_TEMP/firelink-installer" --target ${{ matrix.target }}
|
||||
if (-not $installer) { throw "Windows NSIS installer artifact was not produced." }
|
||||
$extractRoot = "$env:RUNNER_TEMP/firelink-installer"
|
||||
Remove-Item -Recurse -Force $extractRoot -ErrorAction SilentlyContinue
|
||||
7z x $installer.FullName "-o$extractRoot" -y
|
||||
node scripts/verify-binaries.js --search-root "$extractRoot" --target ${{ matrix.target }}
|
||||
$installRoot = "$env:RUNNER_TEMP\FirelinkSmoke"
|
||||
Remove-Item -Recurse -Force $installRoot -ErrorAction SilentlyContinue
|
||||
& $installer.FullName /S "/D=$installRoot"
|
||||
if ($LASTEXITCODE -ne 0) { throw "Silent NSIS install failed with exit code $LASTEXITCODE." }
|
||||
$exe = Get-ChildItem $installRoot -Recurse -File | Where-Object { $_.Name -ieq "firelink.exe" } | Sort-Object FullName | Select-Object -First 1
|
||||
if (-not $exe) { throw "firelink.exe was not found under $installRoot after silent install." }
|
||||
node scripts/smoke-packaged-app.js --executable $exe.FullName --assert-no-visible-child-windows
|
||||
- name: Verify Linux AppImage payload and launch
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
@@ -124,6 +122,7 @@ jobs:
|
||||
name: Firelink-${{ matrix.label }}-${{ github.ref_name }}
|
||||
path: ${{ matrix.artifact }}
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
publish:
|
||||
name: Publish GitHub release
|
||||
|
||||
Generated
+4
-4
@@ -32,7 +32,7 @@
|
||||
"postcss": "^8.5.15",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.1.0",
|
||||
"vite": "^8.1.2",
|
||||
"vitest": "^4.1.9"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1974,9 +1974,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.1.tgz",
|
||||
"integrity": "sha512-X/05/cT+VITy2AeDc1der6smvGWWREtL4hPbPTaVbjSBuuWkmNOjR6HP3NzqcQA2nF6VHGUPaFRJyft/2AE9Kg==",
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.2.tgz",
|
||||
"integrity": "sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@
|
||||
"postcss": "^8.5.15",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.1.0",
|
||||
"vite": "^8.1.2",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
|
||||
function run(command, args) {
|
||||
const executable = process.platform === 'win32' && command === 'npm' ? 'npm.cmd' : command;
|
||||
const result = spawnSync(executable, args, {
|
||||
cwd: repoRoot,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(`Failed to run ${executable}: ${result.error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
run(process.execPath, ['scripts/stage-engines.js']);
|
||||
run(process.execPath, ['scripts/verify-binaries.js', '--staged']);
|
||||
|
||||
if (process.env.FIRELINK_OMIT_ENGINE_DIST_FOR_TAURI_BUNDLE === '1') {
|
||||
const engineDist = path.join(repoRoot, 'src-tauri', 'engine-dist');
|
||||
fs.rmSync(engineDist, { recursive: true, force: true });
|
||||
fs.mkdirSync(engineDist, { recursive: true });
|
||||
console.log('Omitted engine-dist from the initial Tauri bundle; release packaging will repack verified engines.');
|
||||
}
|
||||
|
||||
run(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']);
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { collectRegularFiles, sha256 } from './engine-payload-integrity.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
|
||||
function argValue(name) {
|
||||
const index = process.argv.indexOf(name);
|
||||
return index >= 0 ? process.argv[index + 1] : undefined;
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function mustBeDirectory(directory, label) {
|
||||
if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) {
|
||||
fail(`${label} does not exist or is not a directory: ${directory}`);
|
||||
}
|
||||
}
|
||||
|
||||
function mustBeFile(file, label) {
|
||||
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
||||
fail(`${label} does not exist or is not a file: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
function findSingleAppImage(directory) {
|
||||
mustBeDirectory(directory, 'AppImage bundle directory');
|
||||
const appImages = fs
|
||||
.readdirSync(directory)
|
||||
.filter(name => name.endsWith('.AppImage'))
|
||||
.map(name => path.join(directory, name));
|
||||
|
||||
if (appImages.length !== 1) {
|
||||
fail(`Expected exactly one AppImage in ${directory}, found ${appImages.length}.`);
|
||||
}
|
||||
|
||||
return appImages[0];
|
||||
}
|
||||
|
||||
function validatePayloadManifest(root, target, label) {
|
||||
const manifestPath = path.join(root, 'payload-manifest.json');
|
||||
mustBeFile(manifestPath, `${label} payload manifest`);
|
||||
|
||||
let manifest;
|
||||
try {
|
||||
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
} catch (error) {
|
||||
fail(`${label} payload manifest is not valid JSON: ${error.message}`);
|
||||
}
|
||||
|
||||
if (manifest.target !== target) {
|
||||
fail(`${label} payload target mismatch: expected ${target}, got ${manifest.target}`);
|
||||
}
|
||||
|
||||
const expectedFiles = Object.keys(manifest.files || {}).sort();
|
||||
const actualFiles = collectRegularFiles(root, { ignoredNames: ['payload-manifest.json'] })
|
||||
.map(file => path.relative(root, file).split(path.sep).join('/'))
|
||||
.sort();
|
||||
|
||||
if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
|
||||
fail(`${label} payload files do not match payload-manifest.json.`);
|
||||
}
|
||||
|
||||
for (const relative of expectedFiles) {
|
||||
const file = path.join(root, relative);
|
||||
if (sha256(file) !== manifest.files[relative]) {
|
||||
fail(`${label} payload checksum mismatch: ${relative}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: options.cwd ?? repoRoot,
|
||||
env: { ...process.env, ...options.env },
|
||||
stdio: options.stdio ?? 'inherit',
|
||||
encoding: options.stdio === 'pipe' ? 'utf8' : undefined,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
fail(`Failed to run ${command}: ${result.error.message}`);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
if (options.stdio === 'pipe') {
|
||||
if (result.stdout) {
|
||||
process.stdout.write(result.stdout);
|
||||
}
|
||||
if (result.stderr) {
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
}
|
||||
fail(`${command} exited with status ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
const target = argValue('--target') || process.env.FIRELINK_TARGET_TRIPLE || process.env.TAURI_ENV_TARGET_TRIPLE;
|
||||
if (!target) {
|
||||
fail('Pass --target <triple>.');
|
||||
}
|
||||
|
||||
const bundleDirectory = path.join(repoRoot, 'src-tauri', 'target', target, 'release', 'bundle', 'appimage');
|
||||
const appDir = path.resolve(argValue('--appdir') || path.join(bundleDirectory, 'Firelink.AppDir'));
|
||||
const appImage = path.resolve(argValue('--appimage') || findSingleAppImage(bundleDirectory));
|
||||
const appImageTool = path.resolve(argValue('--appimagetool') || process.env.APPIMAGETOOL || 'appimagetool');
|
||||
const source = path.join(repoRoot, 'src-tauri', 'provisioned-engines', target);
|
||||
const destination = path.join(appDir, 'usr', 'lib', 'Firelink', 'engine-dist', target);
|
||||
|
||||
mustBeDirectory(appDir, 'Firelink.AppDir');
|
||||
mustBeDirectory(source, 'Provisioned engine payload');
|
||||
mustBeFile(appImage, 'AppImage');
|
||||
mustBeFile(appImageTool, 'appimagetool');
|
||||
|
||||
validatePayloadManifest(source, target, 'Provisioned engine');
|
||||
|
||||
fs.rmSync(destination, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||
fs.cpSync(source, destination, {
|
||||
recursive: true,
|
||||
dereference: false,
|
||||
preserveTimestamps: true,
|
||||
});
|
||||
validatePayloadManifest(destination, target, 'AppDir engine');
|
||||
|
||||
fs.chmodSync(appImageTool, 0o755);
|
||||
run(appImageTool, [appDir, appImage], {
|
||||
env: {
|
||||
APPIMAGE_EXTRACT_AND_RUN: '1',
|
||||
ARCH: 'x86_64',
|
||||
},
|
||||
});
|
||||
|
||||
const extractRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'firelink-appimage-'));
|
||||
try {
|
||||
fs.chmodSync(appImage, 0o755);
|
||||
run(appImage, ['--appimage-extract'], {
|
||||
cwd: extractRoot,
|
||||
env: { APPIMAGE_EXTRACT_AND_RUN: '1' },
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
const extractedPayload = path.join(extractRoot, 'squashfs-root', 'usr', 'lib', 'Firelink', 'engine-dist', target);
|
||||
mustBeDirectory(extractedPayload, 'Extracted AppImage engine payload');
|
||||
validatePayloadManifest(extractedPayload, target, 'Extracted AppImage engine');
|
||||
} finally {
|
||||
fs.rmSync(extractRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log(`Repacked and verified AppImage engine payload for ${target}.`);
|
||||
Regular → Executable
+103
-14
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process';
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
|
||||
function argValue(name) {
|
||||
@@ -12,8 +12,9 @@ if (!executableArg) {
|
||||
console.error('Pass --executable <path>.');
|
||||
process.exit(1);
|
||||
}
|
||||
const executable = path.resolve(executableArg);
|
||||
|
||||
const executable = path.resolve(executableArg);
|
||||
const assertNoVisibleChildWindows = process.argv.includes('--assert-no-visible-child-windows');
|
||||
const child = spawn(executable, [], {
|
||||
cwd: process.env.RUNNER_TEMP || process.env.TMPDIR || process.cwd(),
|
||||
detached: process.platform !== 'win32',
|
||||
@@ -21,26 +22,39 @@ const child = spawn(executable, [], {
|
||||
...process.env,
|
||||
FIRELINK_SMOKE_TEST: '1',
|
||||
WEBKIT_DISABLE_COMPOSITING_MODE: '1',
|
||||
GDK_BACKEND: 'x11'
|
||||
GDK_BACKEND: 'x11',
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
let spawnError = null;
|
||||
let readyPort = null;
|
||||
|
||||
child.on('error', error => {
|
||||
spawnError = error;
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (readyPort === null) {
|
||||
console.error(`Child exited prematurely with code ${code} signal ${signal}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', data => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
let readyPort = null;
|
||||
for (let attempt = 0; attempt < 200 && readyPort === null; attempt += 1) {
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function findReadyPort() {
|
||||
for (let attempt = 0; attempt < 200 && readyPort === null; attempt += 1) {
|
||||
if (spawnError) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (let port = 6412; port <= 6422; port += 1) {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/ping`);
|
||||
@@ -50,14 +64,75 @@ for (let attempt = 0; attempt < 200 && readyPort === null; attempt += 1) {
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (readyPort === null) {
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
await sleep(250);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
|
||||
} else {
|
||||
function assertNoVisibleWindows(rootPid) {
|
||||
if (process.platform !== 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
const script = `
|
||||
$root = ${rootPid}
|
||||
$all = Get-CimInstance Win32_Process
|
||||
$pending = @($root)
|
||||
$descendants = @()
|
||||
while ($pending.Count -gt 0) {
|
||||
$parent = $pending[0]
|
||||
if ($pending.Count -eq 1) {
|
||||
$pending = @()
|
||||
} else {
|
||||
$pending = $pending[1..($pending.Count - 1)]
|
||||
}
|
||||
|
||||
$children = @($all | Where-Object { $_.ParentProcessId -eq $parent })
|
||||
foreach ($child in $children) {
|
||||
$descendants += $child.ProcessId
|
||||
$pending += $child.ProcessId
|
||||
}
|
||||
}
|
||||
|
||||
$visible = foreach ($pid in $descendants) {
|
||||
$process = Get-Process -Id $pid -ErrorAction SilentlyContinue
|
||||
if ($process -and $process.MainWindowHandle -ne 0) {
|
||||
"$($process.ProcessName)($pid)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($visible.Count -gt 0) {
|
||||
Write-Error "Visible child process windows detected: $($visible -join ', ')"
|
||||
exit 1
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
execFileSync('powershell', ['-NoProfile', '-Command', script], {
|
||||
stdio: 'pipe',
|
||||
windowsHide: true,
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = [error.stdout?.toString(), error.stderr?.toString()].filter(Boolean).join('\n').trim();
|
||||
throw new Error(detail || 'Visible child process window check failed.');
|
||||
}
|
||||
}
|
||||
|
||||
function terminateChild() {
|
||||
if (!child.pid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(-child.pid, 'SIGTERM');
|
||||
} catch {
|
||||
@@ -65,9 +140,23 @@ if (process.platform === 'win32') {
|
||||
}
|
||||
}
|
||||
|
||||
if (readyPort === null) {
|
||||
const detail = spawnError?.message || stderr.slice(-1000);
|
||||
console.error(`Packaged Firelink did not expose extension server. ${detail}`);
|
||||
await findReadyPort();
|
||||
|
||||
try {
|
||||
if (readyPort === null) {
|
||||
if (spawnError) {
|
||||
console.error(`Packaged Firelink failed to start: ${spawnError.message}`);
|
||||
} else {
|
||||
console.error(`Packaged Firelink did not expose extension ping endpoint. Stderr:\n${stderr.slice(-1000)}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (assertNoVisibleChildWindows) {
|
||||
assertNoVisibleWindows(child.pid);
|
||||
}
|
||||
|
||||
console.log(`Packaged Firelink smoke passed on 127.0.0.1:${readyPort}`);
|
||||
} finally {
|
||||
terminateChild();
|
||||
}
|
||||
console.log(`Packaged Firelink smoke passed on 127.0.0.1:${readyPort}`);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"build": {
|
||||
"beforeDevCommand": "node scripts/stage-engines.js && npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "node scripts/stage-engines.js && node scripts/verify-binaries.js --staged && npm run build",
|
||||
"beforeBuildCommand": "node scripts/before-tauri-build.js",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
|
||||
Reference in New Issue
Block a user