diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9839f660..86307017 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/app-el-prep.js b/app-el-prep.js deleted file mode 100644 index 485c865d..00000000 --- a/app-el-prep.js +++ /dev/null @@ -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 }); diff --git a/app-el.js b/app-el.js index a3d00863..ab86241d 100644 --- a/app-el.js +++ b/app-el.js @@ -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 }); diff --git a/app.js b/app.js index 24fa5c00..b9705dcd 100644 --- a/app.js +++ b/app.js @@ -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}); diff --git a/bin/GS.icns b/bin/GS.icns new file mode 100644 index 00000000..cc700c78 Binary files /dev/null and b/bin/GS.icns differ diff --git a/bin/GS.ico b/bin/GS.ico new file mode 100644 index 00000000..6ff34f32 Binary files /dev/null and b/bin/GS.ico differ diff --git a/bin/GS.png b/bin/GS.png new file mode 100644 index 00000000..5ca332f2 Binary files /dev/null and b/bin/GS.png differ diff --git a/bin/electron-entitlements.plist b/bin/electron-entitlements.plist new file mode 100644 index 00000000..a14fdccf --- /dev/null +++ b/bin/electron-entitlements.plist @@ -0,0 +1,22 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.debugger + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.app-sandbox + + com.apple.security.inherit + + com.apple.security.network.client + + + diff --git a/bin/electron-notarize.js b/bin/electron-notarize.js new file mode 100644 index 00000000..fbfc7eb4 --- /dev/null +++ b/bin/electron-notarize.js @@ -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'); +}; diff --git a/bin/electron-post.js b/bin/electron-post.js new file mode 100644 index 00000000..b08c5f04 --- /dev/null +++ b/bin/electron-post.js @@ -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)); diff --git a/bin/electron-pre.js b/bin/electron-pre.js new file mode 100644 index 00000000..1b85bb86 --- /dev/null +++ b/bin/electron-pre.js @@ -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 +}); diff --git a/bin/github-getver.js b/bin/github-getver.js new file mode 100644 index 00000000..a34ee0b3 --- /dev/null +++ b/bin/github-getver.js @@ -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`); +} diff --git a/bin/install-pre.js b/bin/install-pre.js new file mode 100644 index 00000000..fe7b118f --- /dev/null +++ b/bin/install-pre.js @@ -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)); diff --git a/bin/npm-post b/bin/npm-post deleted file mode 100755 index e59ea326..00000000 --- a/bin/npm-post +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -[ -d tmp ] && rm -rf tmp - diff --git a/bin/npm-pre b/bin/npm-pre deleted file mode 100755 index 5478992e..00000000 --- a/bin/npm-pre +++ /dev/null @@ -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 - diff --git a/links.csv b/links.csv new file mode 100644 index 00000000..c8d853b1 --- /dev/null +++ b/links.csv @@ -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/ diff --git a/package.json b/package.json index 5d1ca35c..8f0166ee 100644 --- a/package.json +++ b/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/src/kiri/init.js b/src/kiri/init.js index 5a8c2d6c..6a77e3a1 100644 --- a/src/kiri/init.js +++ b/src/kiri/init.js @@ -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'; }