allow manual triggering of builds

bump action versions
add token reference for electron build
add release workflow
add links for windows replacements
add helper for github build workflow
align asset names with build artifact names
produce zip artifacts from electron build
change artifact name to KiriMoto and add array build target
add mac app signing
add electron app icons
add links.csv helper
add dryrun cache build and integration into electron app for faster startup
allow windows install alt location. suppress gdpr on localhost / built apps
This commit is contained in:
Stewart Allen 2024-06-14 17:48:01 -04:00
commit b526bec87e
18 changed files with 372 additions and 59 deletions

View file

@ -1,24 +1,24 @@
name: Build Electron App
name: Build and Release Electron App
on:
push:
tags:
- 'v*'
workflow_dispatch: # Allows manual triggering
jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '20'
@ -26,11 +26,98 @@ jobs:
run: npm install
- name: Build Electron app
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: kiri-${{ matrix.os }}-${{ github.ref_name }}
path: dist/
- name: Display Build Artifacts
run: ls -l dist
- name: Upload Linux artifact
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: build-linux
path: dist/*.zip
- name: Upload Windows artifact
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: build-win
path: dist/*.exe
- name: Upload Mac artifact
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: build-mac
path: dist/*.dmg
- name: Sha256 Mac artifact
if: matrix.os == 'macos-latest'
run: shasum -a 256 dist/*.dmg
create_release:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Create release name
run: node bin/github-getver.js "${{ github.event_name }}"
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1.1.4
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
with:
tag_name: ${{ env.TAG_NAME }}
release_name: Release ${{ env.TAG_NAME }}
draft: false
prerelease: false
- name: Download All Release Assets
uses: actions/download-artifact@v4.1.7
with:
path: artifacts
merge-multiple: true
- name: Display Downloaded Assets
run: ls -ltR artifacts
- name: Zip Windows artifact
run: zip -r artifacts/KiriMoto-win-x64.zip artifacts/KiriMoto-win-x64.exe
- name: Upload Release Asset (Linux)
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: artifacts/KiriMoto-linux-x64.zip
asset_name: KiriMoto-Ubuntu-x64-${{ env.TAG_NAME }}.zip
asset_content_type: application/zip
- name: Upload Release Asset (macOS)
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: artifacts/KiriMoto-mac-arm64.dmg
asset_name: KiriMoto-MacOS-arm-${{ env.TAG_NAME }}.dmg
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows)
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: artifacts/KiriMoto-win-x64.zip
asset_name: KiriMoto-Win-x64-${{ env.TAG_NAME }}.zip
asset_content_type: application/zip

View file

@ -1,10 +0,0 @@
const fs = require('fs-extra');
const path = require('path');
const srcDir = path.join(__dirname, 'src');
const srcTmp = path.join(__dirname, 'tmp/src');
fs.copySync(srcDir, srcTmp, { dereference: true });
const webDir = path.join(__dirname, 'web');
const webTmp = path.join(__dirname, 'tmp/web');
fs.copySync(webDir, webTmp, { dereference: true });

View file

@ -4,11 +4,11 @@ const server = require('@gridspace/app-server');
const basDir = __dirname;
const usrDir = app.getPath("userData");
const appDir = path.join(usrDir, 'apps/gs');
const appDir = path.join(usrDir, 'gapp');
const cnfDir = path.join(appDir, 'conf');
const logDir = path.join(appDir, 'logs');
const datDir = path.join(appDir, 'data');
const debug = process.argv.slice(2).map(v => v.replaceAll('-','')).contains('debug');
const debug = process.argv.slice(2).map(v => v.replaceAll('-','')).contains('debugg');
const devel = process.argv.slice(2).map(v => v.replaceAll('-','')).contains('devel');
// console.log({ appDir, usrDir, logDir, datDir, basDir });
@ -26,6 +26,7 @@ server({
data: datDir,
conf: cnfDir,
logs: logDir,
cache: path.join(basDir,"data","cache"),
debug
});

31
app.js
View file

@ -14,6 +14,7 @@ const agent = require('express-useragent');
const license = require_fresh('./src/moto/license.js');
const version = license.VERSION || "rogue";
const netdb = require('@gridspace/net-level-client');
const PATH = require('path');
const fileCache = {};
const code_src = {};
@ -23,6 +24,7 @@ const load = [];
const synth = {};
const api = {};
let forceUseCache = false;
let serviceWorker = true;
let crossOrigin = false;
let setupFn;
@ -74,10 +76,13 @@ function init(mod) {
dir = mod.dir;
log = mod.log;
if (mod.env.single) console.log({ cwd: process.cwd(), env: mod.env });
dversion = debug ? `_${version}` : version;
cacheDir = mod.util.datadir("cache");
cacheDir = mod.env.cache || mod.util.datadir("cache");
if (mod.env.single) logger.log({ cacheDir });
forceUseCache = mod.env.cache ? true : false;
const approot = "main/gapp";
const approot = PATH.join("main","gapp");
const refcache = {};
const callstack = [];
let xxxx = false;
@ -98,7 +103,7 @@ function init(mod) {
uses: [],
deps: [ approot ]
};
let full = `${dir}/src/${path}.js`;
let full = PATH.join(dir,"src",`${path}.js`);
try {
fs.lstatSync(full);
} catch (e) {
@ -369,7 +374,7 @@ function initModule(mod, file, dir) {
mod.static(pre || "/", root);
},
code: (endpoint, path) => {
let fpath = mod.dir + "/" + path;
let fpath = PATH.join(mod.dir, path);
if (debug) {
code[endpoint] = fs.readFileSync(fpath);
} else {
@ -515,7 +520,7 @@ function handleOptions(req, res, next) {
function handleWasm(req, res, next) {
let file = req.app.path.split('/').pop();
let ext = (file || '').split('.')[1];
let path = `${dir}/src/wasm/${file}`;
let path = PATH.join(dir,"src","wasm",file);
let mod = lastmod(path);
if (ext === 'wasm' && mod) {
@ -588,7 +593,7 @@ function serveCode(req, res, code) {
}
function generateIcons() {
let root = `${dir}/src/kiri-ico`;
let root = PATH.join(dir,"src","kiri-ico");
let icos = {};
fs.readdirSync(root).forEach(file => {
let name = file.split(".")[0] ;
@ -598,12 +603,12 @@ function generateIcons() {
}
function generateDevices() {
let root = `${dir}/src/kiri-dev`;
let root = PATH.join(dir,"src","kiri-dev");
let devs = {};
fs.readdirSync(root).forEach(type => {
let map = devs[type] = devs[type] || {};
fs.readdirSync(`${root}/${type}`).forEach(device => {
map[device] = JSON.parse(fs.readFileSync(`${root}/${type}/${device}`));
fs.readdirSync(PATH.join(root,type)).forEach(device => {
map[device] = JSON.parse(fs.readFileSync(PATH.join(root,type,device)));
});
});
// console.log({ devs });
@ -657,7 +662,7 @@ function concatCode(array) {
direct.forEach(file => {
let cached = getCachedFile(file, function(path) {
return minify(`${dir}/${file}`);
return minify(PATH.join(dir,file));
});
if (oversion) {
cached = `self.debug_version='${oversion}';self.enable_service=${serviceWorker};` + cached;
@ -673,8 +678,8 @@ function concatCode(array) {
}
function getCachedFile(file, fn) {
let filePath = `${dir}/${file}`;
let cachePath = cacheDir + "/" + file
let filePath = PATH.join(dir,file);
let cachePath = cacheDir + PATH.sep + file
.replace(/\//g,'_')
.replace(/\\/g,'_')
.replace(/:/g,'_'),
@ -702,7 +707,7 @@ function getCachedFile(file, fn) {
cmod = lastmod(cachePath),
cacheData;
if (cmod >= smod) {
if (cmod >= smod || (forceUseCache && cmod)) {
cacheData = fs.readFileSync(cachePath);
} else {
logger.log({update_cache:filePath});

BIN
bin/GS.icns Normal file

Binary file not shown.

BIN
bin/GS.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
bin/GS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -0,0 +1,22 @@
<?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>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.inherit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

24
bin/electron-notarize.js Normal file
View file

@ -0,0 +1,24 @@
require('dotenv').config();
const { notarize } = require('@electron/notarize');
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
if (process.env.SKIPNOT || electronPlatformName !== 'darwin') {
return;
}
const appName = context.packager.appInfo.productFilename;
console.log(' ** notarizing:', appName);
const result = await notarize({
appBundleId: 'space.grid.kiri',
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
});
console.log(' ** notarizing complete:', result || 'no process output');
};

17
bin/electron-post.js Normal file
View file

@ -0,0 +1,17 @@
const fs = require('fs-extra');
async function removeDirectory(dirPath) {
try {
await fs.remove(dirPath);
console.log('Directory removed successfully!');
} catch (err) {
console.error('Error removing directory:', err);
}
}
async function main() {
console.log('npm post running');
await removeDirectory("tmp");
}
main().catch(err => console.error('Error', err));

16
bin/electron-pre.js Normal file
View file

@ -0,0 +1,16 @@
const fs = require('fs-extra');
const path = require('path');
const server = require('@gridspace/app-server');
// deref src and web for windows
const srcTmp = path.join('tmp','src');
fs.copySync("src", srcTmp, { dereference: true });
const webTmp = path.join('tmp','web');
fs.copySync("web", webTmp, { dereference: true });
// pre-build asset cache
server({
dryrun: true,
single: true
});

18
bin/github-getver.js Normal file
View file

@ -0,0 +1,18 @@
// create version for release for github build workflow
const fs = require('fs');
const type = process.argv[2];
if (type && process.env.GITHUB_ENV) {
const pkgVer = JSON.parse(fs.readFileSync('package.json', 'utf8')).version;
const rando = ((Math.random() * 0xfff) & 0xfff).toString().padStart(4,0);
const releaseTag =
type === 'workflow_dispatch' ? `${pkgVer}.${rando}` :
type === 'push' ? pkgVer :
(`rogue-` + ( (Math.random() * 0xfffff) & 0xfffff ))
console.log({ type, version: pkgVer, releaseTag });
// write version to GITHUB_ENV file
fs.appendFileSync(process.env.GITHUB_ENV, `TAG_NAME=${releaseTag}\n`);
}

81
bin/install-pre.js Normal file
View file

@ -0,0 +1,81 @@
const os = require('os');
const fs = require('fs-extra');
const fetchr = import('node-fetch');
const path = require('path');
async function download(url, filePath) {
const fetch = (await fetchr).default;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}
fs.ensureDir(path.dirname(filePath));
const fileStream = fs.createWriteStream(filePath);
return new Promise((resolve, reject) => {
response.body.pipe(fileStream);
response.body.on('error', reject);
fileStream.on('error', error => {
console.log({ error });
});
fileStream.on('finish', () => {
resolve();
});
});
}
async function main() {
console.log('npm pre running');
await download(
"https://static.grid.space/gapp/manifold.js",
path.join("src", "ext", "manifold.js")
);
await download(
"https://static.grid.space/gapp/manifold.wasm",
path.join("src", "wasm", "manifold.wasm")
);
const links = fs.readFileSync("links.csv")
.toString()
.trim()
.split('\n')
.map(line => line.trim())
.map(line => line.split(',').map(v => v.trim()));
if (os.platform() === 'win32')
for (let [link, target] of links) {
const absoluteTarget = path.resolve(path.dirname(link), target);
// console.log({ link, target, absoluteTarget });
try {
// Remove existing link if it exists
// if (fs.existsSync(link)) {
// console.log({ unlink: link });
// fs.unlinkSync(link);
// } else {
// console.log('no file', link);
// }
console.log({ win32_replace: link });
await fs.remove(link).catch(error => console.log({ remove_error: error }));
// console.log({ copy: absoluteTarget, to: link });
await fs.copy(absoluteTarget, link, { dereference: true }).catch(error => console.log({ copy_error: error }));
// const targetStats = fs.lstatSync(absoluteTarget);
// let type = targetStats.isDirectory() ? 'junction' : 'file';
// // Create the symlink
// console.log(`relink: ${link} as ${type}`);
// fs.symlinkSync(absoluteTarget, link, type);
} catch (err) {
// console.error(`Error creating symlink: ${link} -> ${target}`, err);
console.error(`Error creating symlink: ${link} -> ${absoluteTarget}`, err);
}
}
}
main().catch(err => console.error('Error', err));

View file

@ -1,4 +0,0 @@
#!/bin/sh
[ -d tmp ] && rm -rf tmp

View file

@ -1,5 +0,0 @@
#!/bin/sh
( curl https://static.grid.space/gapp/manifold.js > src/ext/manifold.js ) > /dev/null 2>&1
( curl https://static.grid.space/gapp/manifold.wasm > src/wasm/manifold.wasm ) > /dev/null 2>&1

18
links.csv Normal file
View file

@ -0,0 +1,18 @@
src/ext/three-svg.js,../../node_modules/three/examples/js/loaders/SVGLoader.js
src/ext/three.js,../../node_modules/three/build/three.min.js
src/ext/base64.js,../../node_modules/base64-js/base64js.min.js
src/ext/earcut.js,../../node_modules/earcut/src/earcut.js
src/ext/three-bvh.js,../../node_modules/three-mesh-bvh/build/index.umd.cjs
src/ext/tween.js,../../node_modules/@tweenjs/tween.js/src/Tween.js
src/ext/three-bgu.js,../../node_modules/three/examples/js/utils/BufferGeometryUtils.js
src/ext/jszip.js,../../node_modules/jszip/dist/jszip.js
src/kiri-dev/fdm/GridBot.Two,GridBot.One
src/kiri/lang-en.js,../../web/kiri/lang/en.js
web/kiri/lang/pl.js,pl-pl.js
web/kiri/lang/pt-pt.js,pt.js
web/kiri/lang/da-dk.js,da.js
web/kiri/lang/en-us.js,en.js
web/kiri/lang/fr-fr.js,fr.js
web/kiri/lang/de.js,de-de.js
web/kiri/lang/es.js,es-es.js
web/font,../node_modules/@fortawesome/fontawesome-free/
1 src/ext/three-svg.js ../../node_modules/three/examples/js/loaders/SVGLoader.js
2 src/ext/three.js ../../node_modules/three/build/three.min.js
3 src/ext/base64.js ../../node_modules/base64-js/base64js.min.js
4 src/ext/earcut.js ../../node_modules/earcut/src/earcut.js
5 src/ext/three-bvh.js ../../node_modules/three-mesh-bvh/build/index.umd.cjs
6 src/ext/tween.js ../../node_modules/@tweenjs/tween.js/src/Tween.js
7 src/ext/three-bgu.js ../../node_modules/three/examples/js/utils/BufferGeometryUtils.js
8 src/ext/jszip.js ../../node_modules/jszip/dist/jszip.js
9 src/kiri-dev/fdm/GridBot.Two GridBot.One
10 src/kiri/lang-en.js ../../web/kiri/lang/en.js
11 web/kiri/lang/pl.js pl-pl.js
12 web/kiri/lang/pt-pt.js pt.js
13 web/kiri/lang/da-dk.js da.js
14 web/kiri/lang/en-us.js en.js
15 web/kiri/lang/fr-fr.js fr.js
16 web/kiri/lang/de.js de-de.js
17 web/kiri/lang/es.js es-es.js
18 web/font ../node_modules/@fortawesome/fontawesome-free/

View file

@ -20,9 +20,12 @@
"gcode",
"slicer"
],
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.1.1",
"@gridspace/app-server": "^0.0.10",
"@gridspace/app-server": "^0.0.13",
"@gridspace/net-level-client": "^0.2.3",
"@tweenjs/tween.js": "^16.6.0",
"base64-js": "^1.5.1",
@ -31,7 +34,6 @@
"connect": "^3.7.0",
"earcut": "^2.2.3",
"express-useragent": "^1.0.13",
"fs-extra": "^11.2.0",
"jszip": "^3.7.1",
"moment": "^2.29.4",
"serve-static": "^1.14.1",
@ -42,31 +44,50 @@
"ws": "^7.5.5"
},
"devDependencies": {
"@electron/notarize": "latest",
"dotenv": "latest",
"electron": "latest",
"electron-builder": "latest"
"electron-builder": "latest",
"fs-extra": "^11.2.0",
"node-fetch": "^3.3.2"
},
"scripts": {
"start": "electron .",
"start-dev": "electron . --devel",
"start-dbg": "electron . --debugg",
"start-ddb": "electron . --devel --debugg",
"build": "npm run prebuild && electron-builder",
"build-debug": "npm run prebuild && DEBUG=electron-builder electron-builder",
"prebuild": "node app-el-prep.js",
"postbuild": "bin/npm-post",
"preinstall": "bin/npm-pre"
"build-linux": "npm run build -- --linux --x64",
"build-win": "npm run build -- --win --x64",
"build-mac": "npm run build -- --mac --arm64",
"mklinks": "find src web -type l | xargs -I{} sh -c 'echo \"{},$(readlink {})\"' > links.csv",
"mac-verify": "spctl --assess -vv --type install dist/*/*.app",
"prebuild": "node bin/electron-pre.js",
"postbuild": "node bin/electron-post.js",
"preinstall": "node bin/install-pre.js"
},
"main": "app-el.js",
"build": {
"appId": "space.grid.kiri",
"productName": "KiriMoto",
"artifactName": "KiriMoto-${os}-${arch}.${ext}",
"files": [
{
"from": "tmp/src",
"to": "src",
"filter": [ "**/*" ]
"filter": [
"**/*"
]
},
{
"from": "tmp/web",
"to": "web",
"filter": [ "**/*" ]
"filter": [
"**/*"
]
},
"bin/*",
"conf/**/*",
"data/**/*",
"dist/**/*",
@ -74,19 +95,40 @@
"app.js",
"package.json"
],
"extraFiles": [
],
"extraFiles": [],
"directories": {
"output": "dist"
},
"win": {
"target": "nsis"
"icon": "bin/GS.ico",
"target": [
"nsis",
"zip"
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"allowElevation": true
},
"mac": {
"target": "dmg"
"icon": "bin/GS.icns",
"category": "public.app-category.utilities",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "bin/electron-entitlements.plist",
"entitlementsInherit": "bin/electron-entitlements.plist",
"target": [
"zip"
]
},
"linux": {
"target": "AppImage"
}
"icon": "bin/GS.png",
"target": [
"AppImage",
"zip"
]
},
"afterSign": "bin/electron-notarize.js"
}
}
}

View file

@ -2624,7 +2624,8 @@ gapp.register("kiri.init", [], (root, exports) => {
api.event.emit('init-done', stats);
// show gdpr if it's never been seen and we're not iframed
if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !api.const.LOCAL) {
const isLocal = api.const.LOCAL || WIN.location.host.split(':')[0] === 'localhost';
if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) {
$('gdpr').style.display = 'flex';
}