diff --git a/.gitattributes b/.gitattributes index 85a238a..372933e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,5 @@ # the cmd one CRLF, or they fail to run where they are packaged. build/cli/automata text eol=lf build/cli/automata.cmd text eol=crlf +# The .deb package scripts run under bash on the target machine. +build/linux/*.sh text eol=lf diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index 2092af8..8e824c5 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -101,6 +101,60 @@ jobs: # hunting for a macOS identity and failing the build over it. CSC_IDENTITY_AUTO_DISCOVERY: false + # The installers put the `automata` command line on PATH: the Windows one + # adds resources\cli to the user's PATH (build/installer.nsh), the .deb + # links /usr/local/bin/automata (build/linux/after-install.sh). Each is + # checked on the real package — installed, run from PATH, uninstalled, and + # checked gone — because a mistake in either only shows on a real install. + - name: "Installed, automata is on PATH (Windows)" + if: matrix.platform == 'win' + shell: pwsh + run: | + $setup = Get-ChildItem release\*.exe | Select-Object -First 1 + Start-Process $setup.FullName -ArgumentList '/S' -Wait + $install = Join-Path $env:LOCALAPPDATA 'Programs\AutomataStudio' + $dir = Join-Path $install 'resources\cli' + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + if (-not (($userPath -split ';') -contains $dir)) { throw "The user PATH does not hold $dir`n$userPath" } + $env:Path = "$userPath;$env:Path" + automata --version + if ($LASTEXITCODE -ne 0) { throw "automata --version exited $LASTEXITCODE" } + automata run js/examples/dfa.json 101 0 + if ($LASTEXITCODE -ne 0) { throw "automata run exited $LASTEXITCODE" } + # The uninstaller copies itself away and relaunches, so -Wait returns + # early; wait for the folder to go instead. + $uninstaller = Get-ChildItem $install -Filter 'Uninstall*.exe' | Select-Object -First 1 + Start-Process $uninstaller.FullName -ArgumentList '/S' -Wait + for ($i = 0; $i -lt 90 -and (Test-Path $dir); $i++) { Start-Sleep 1 } + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + if (($userPath -split ';') -contains $dir) { throw "Uninstalling left $dir on the user PATH" } + + - name: "Installed, automata is on PATH (Linux .deb)" + if: matrix.platform == 'linux' + run: | + deb=$(ls release/*.deb | head -1) + sudo apt-get install -y "./$deb" + command -v automata + automata --version + automata run js/examples/dfa.json 101 0 + sudo apt-get remove -y "$(dpkg-deb -f "$deb" Package)" + if [ -e /usr/local/bin/automata ] || [ -L /usr/local/bin/automata ]; then + echo "::error::removing the package left /usr/local/bin/automata"; exit 1 + fi + + # macOS has no install step to hook (a .dmg is drag and drop); the app + # menu's "Install 'automata' Command in PATH" makes the link. What can be + # checked here is that the launcher finds the app through a chain of links. + - name: "The launcher works through a symlink (macOS)" + if: matrix.platform == 'mac' + run: | + app=$(find release -maxdepth 2 -name 'AutomataStudio.app' -type d | head -1) + mkdir -p "$RUNNER_TEMP/bin" "$RUNNER_TEMP/bin2" + ln -s "$PWD/$app/Contents/Resources/cli/automata" "$RUNNER_TEMP/bin/automata" + ln -s ../bin/automata "$RUNNER_TEMP/bin2/automata" + "$RUNNER_TEMP/bin2/automata" --version + "$RUNNER_TEMP/bin2/automata" run js/examples/dfa.json 101 0 + # Straight onto the draft release. The empty-glob check replaces the # if-no-files-found: error this step used to get from upload-artifact, and # --clobber lets a re-run of one failed platform replace its own assets diff --git a/build/cli/automata b/build/cli/automata index 75bd62d..11772c5 100755 --- a/build/cli/automata +++ b/build/cli/automata @@ -6,7 +6,17 @@ # → Contents/MacOS/AutomataStudio # Linux /resources/cli/automata → / set -e -here="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")" && pwd)" +# Follow the link from /usr/local/bin (or wherever it was put) back to this +# file, one step at a time: readlink -f is missing before macOS 12.3. +self="$0" +while [ -L "$self" ]; do + link="$(readlink "$self")" + case "$link" in + /*) self="$link" ;; + *) self="$(dirname "$self")/$link" ;; + esac +done +here="$(cd "$(dirname "$self")" && pwd)" resources="$(dirname "$here")" cli="$resources/app.asar.unpacked/dist-cli/automata.mjs" for exe in "$resources/../MacOS/AutomataStudio" "$resources/../AutomataStudio" "$resources/../automatastudio" "$resources/../automata-studio"; do diff --git a/build/installer.nsh b/build/installer.nsh new file mode 100644 index 0000000..7d72665 --- /dev/null +++ b/build/installer.nsh @@ -0,0 +1,34 @@ +; Windows installer hooks (electron-builder's NSIS include, package.json build.nsis.include). +; +; Puts the `automata` command line on the user's PATH: install adds +; $INSTDIR\resources\cli (which holds automata.cmd) and uninstall takes it off. +; The editing is done by resources\cli-path.ps1 rather than in NSIS, whose +; strings stop at 1024 characters, so a long PATH read into one would come back +; truncated and the user would lose entries. +; +; The install is per-user (one-click), so this is the user's PATH, in HKCU, and +; needs no elevation. A failure is logged and never fails the install: the app +; works without the command line, and the docs say how to add it by hand. + +!macro automataCliPath ARGS + nsExec::ExecToLog '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$INSTDIR\resources\cli-path.ps1" -Dir "$INSTDIR\resources\cli" ${ARGS}' + Pop $0 + ${if} $0 != 0 + DetailPrint "Could not update PATH for the automata command line (exit $0)." + ${endif} + ; WM_SETTINGCHANGE to every top-level window, so Explorer (and so every + ; terminal started from now on) picks up the new PATH without a sign-out. + SendMessage 0xFFFF 0x1A 0 "STR:Environment" /TIMEOUT=5000 +!macroend + +!macro customInstall + !insertmacro automataCliPath "" +!macroend + +!macro customUnInstall + ; An update runs the old version's uninstaller before installing the new one. + ; The folder is the same, so leave PATH alone rather than remove and re-add it. + ${ifNot} ${isUpdated} + !insertmacro automataCliPath "-Remove" + ${endif} +!macroend diff --git a/build/linux/after-install.sh b/build/linux/after-install.sh new file mode 100644 index 0000000..ba74902 --- /dev/null +++ b/build/linux/after-install.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +if type update-alternatives >/dev/null 2>&1; then + # Remove previous link if it doesn't use update-alternatives + if [ -L '/usr/bin/${executable}' -a -e '/usr/bin/${executable}' -a "`readlink '/usr/bin/${executable}'`" != '/etc/alternatives/${executable}' ]; then + rm -f '/usr/bin/${executable}' + fi + update-alternatives --install '/usr/bin/${executable}' '${executable}' '/opt/${sanitizedProductName}/${executable}' 100 || ln -sf '/opt/${sanitizedProductName}/${executable}' '/usr/bin/${executable}' +else + ln -sf '/opt/${sanitizedProductName}/${executable}' '/usr/bin/${executable}' +fi + +# Check if user namespaces are supported by the kernel and working with a quick test: +if ! { [[ -L /proc/self/ns/user ]] && unshare --user true; }; then + # Use SUID chrome-sandbox only on systems without user namespaces: + chmod 4755 '/opt/${sanitizedProductName}/chrome-sandbox' || true +else + chmod 0755 '/opt/${sanitizedProductName}/chrome-sandbox' || true +fi + +if hash update-mime-database 2>/dev/null; then + update-mime-database /usr/share/mime || true +fi + +if hash update-desktop-database 2>/dev/null; then + update-desktop-database /usr/share/applications || true +fi + +# Install apparmor profile. (Ubuntu 24+) +# First check if the version of AppArmor running on the device supports our profile. +# This is in order to keep backwards compatibility with Ubuntu 22.04 which does not support abi/4.0. +# In that case, we just skip installing the profile since the app runs fine without it on 22.04. +# +# Those apparmor_parser flags are akin to performing a dry run of loading a profile. +# https://wiki.debian.org/AppArmor/HowToUse#Dumping_profiles +# +# Unfortunately, at the moment AppArmor doesn't have a good story for backwards compatibility. +# https://askubuntu.com/questions/1517272/writing-a-backwards-compatible-apparmor-profile +if apparmor_status --enabled > /dev/null 2>&1; then + APPARMOR_PROFILE_SOURCE='/opt/${sanitizedProductName}/resources/apparmor-profile' + APPARMOR_PROFILE_TARGET='/etc/apparmor.d/${executable}' + if apparmor_parser --skip-kernel-load --debug "$APPARMOR_PROFILE_SOURCE" > /dev/null 2>&1; then + cp -f "$APPARMOR_PROFILE_SOURCE" "$APPARMOR_PROFILE_TARGET" + + # Updating the current AppArmor profile is not possible and probably not meaningful in a chroot'ed environment. + # Use cases are for example environments where images for clients are maintained. + # There, AppArmor might correctly be installed, but live updating makes no sense. + if ! { [ -x '/usr/bin/ischroot' ] && /usr/bin/ischroot; } && hash apparmor_parser 2>/dev/null; then + # Extra flags taken from dh_apparmor: + # > By using '-W -T' we ensure that any abstraction updates are also pulled in. + # https://wiki.debian.org/AppArmor/Contribute/FirstTimeProfileImport + apparmor_parser --replace --write-cache --skip-read-cache "$APPARMOR_PROFILE_TARGET" + fi + else + echo "Skipping the installation of the AppArmor profile as this version of AppArmor does not seem to support the bundled profile" + fi +fi + +# ── AutomataStudio: the `automata` command line ───────────────────── +# Everything above is electron-builder's own after-install script, copied +# verbatim from app-builder-lib/templates/linux/after-install.tpl: setting +# deb.afterInstall replaces it rather than adding to it, so it has to stay here. +# +# Link the launcher into /usr/local/bin, which is on every user's PATH. A file +# already there that is not our link (say, `automata` installed from npm) is +# left alone: the package should not take a name the user chose to put there. +AUTOMATA_CLI='/opt/${sanitizedProductName}/resources/cli/automata' +AUTOMATA_LINK='/usr/local/bin/automata' +if [ ! -e "$AUTOMATA_LINK" ] && [ ! -L "$AUTOMATA_LINK" ]; then + mkdir -p /usr/local/bin && ln -s "$AUTOMATA_CLI" "$AUTOMATA_LINK" || true +elif [ -L "$AUTOMATA_LINK" ] && [ "$(readlink "$AUTOMATA_LINK")" = "$AUTOMATA_CLI" ]; then + : # already ours +else + echo "Not linking $AUTOMATA_LINK: something else is already there. Run $AUTOMATA_CLI directly, or link it under another name." +fi diff --git a/build/linux/after-remove.sh b/build/linux/after-remove.sh new file mode 100644 index 0000000..59cd70e --- /dev/null +++ b/build/linux/after-remove.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Delete the link to the binary +# update-alternatives --remove : 'path' must be the registered alternative binary, +# not the generic symlink — see https://man7.org/linux/man-pages/man1/update-alternatives.1.html +if type update-alternatives >/dev/null 2>&1; then + update-alternatives --remove '${executable}' '/opt/${sanitizedProductName}/${executable}' +else + rm -f '/usr/bin/${executable}' +fi + +APPARMOR_PROFILE_DEST='/etc/apparmor.d/${executable}' + +# Remove and unload apparmor profile. +if [ -f "$APPARMOR_PROFILE_DEST" ]; then + # Unload the profile from the running kernel before deleting the file so the + # policy is not left enforced until the next reboot. Mirror the chroot guard + # used in the after-install script — live AppArmor operations are not + # meaningful inside a chroot. + # https://wiki.debian.org/AppArmor/HowToUse + if apparmor_status --enabled > /dev/null 2>&1; then + if ! { [ -x '/usr/bin/ischroot' ] && /usr/bin/ischroot; } && hash apparmor_parser 2>/dev/null; then + apparmor_parser --remove "$APPARMOR_PROFILE_DEST" || true + fi + fi + rm -f "$APPARMOR_PROFILE_DEST" +fi + +# ── AutomataStudio: the `automata` command line ───────────────────── +# Everything above is electron-builder's own after-remove script, copied +# verbatim from app-builder-lib/templates/linux/after-remove.tpl, for the same +# reason as in after-install.sh. The link is removed only if it is still ours. +AUTOMATA_CLI='/opt/${sanitizedProductName}/resources/cli/automata' +AUTOMATA_LINK='/usr/local/bin/automata' +if [ -L "$AUTOMATA_LINK" ] && [ "$(readlink "$AUTOMATA_LINK")" = "$AUTOMATA_CLI" ]; then + rm -f "$AUTOMATA_LINK" +fi diff --git a/build/win/cli-path.ps1 b/build/win/cli-path.ps1 new file mode 100644 index 0000000..dbd246a --- /dev/null +++ b/build/win/cli-path.ps1 @@ -0,0 +1,42 @@ +# Puts the automata command line's folder on the user's PATH, or takes it off. +# Run by the installer (build/installer.nsh) on install and on uninstall. +# +# It edits HKCU\Environment directly rather than through +# [Environment]::SetEnvironmentVariable, because that call reads PATH with its +# %VARIABLES% already expanded and writes it back as a plain string — which +# flattens every %USERPROFILE%\… entry the user had into a fixed path. Here the +# value is read unexpanded and written back with the kind it already had. +# +# Adding is idempotent (the folder is never listed twice, whatever its case or +# trailing backslash), and removing takes out only this folder. The installer +# broadcasts WM_SETTINGCHANGE afterwards, so terminals opened from then on see +# the change without signing out. +# +# -Key exists for testing against a scratch key instead of the real one. + +param( + [Parameter(Mandatory = $true)][string]$Dir, + [switch]$Remove, + [string]$Key = 'Environment' +) + +$ErrorActionPreference = 'Stop' + +$k = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($Key) +try { + $had = $k.GetValueNames() -contains 'Path' + $old = if ($had) { [string]$k.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } else { '' } + $kind = if ($had) { $k.GetValueKind('Path') } else { [Microsoft.Win32.RegistryValueKind]::ExpandString } + + $want = $Dir.Trim().TrimEnd('\') + $keep = @($old -split ';' | Where-Object { $_.Trim() -and ($_.Trim().TrimEnd('\') -ine $want) }) + if (-not $Remove) { $keep += $want } + $new = $keep -join ';' + + if ($new -ne $old) { + if ($new) { $k.SetValue('Path', $new, $kind) } + elseif ($had) { $k.DeleteValue('Path', $false) } + } +} finally { + $k.Close() +} diff --git a/docs/cli.md b/docs/cli.md index d31155f..9c66cd3 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -26,13 +26,14 @@ This guide walks through it by task. Every option of every command is in the [co ## Install -**With the desktop app.** The CLI ships inside it and runs on the app's own executable, so nothing else is needed. The installer does not change your `PATH`; do it once: +**With the desktop app.** The CLI ships inside it and runs on the app's own executable, so nothing else is needed. The installers put `automata` on your `PATH`: -| Platform | Put this on your `PATH` | +| Platform | How `automata` gets on your `PATH` | | --- | --- | -| Windows | the folder `%LOCALAPPDATA%\Programs\AutomataStudio\resources\cli` (it holds `automata.cmd`) | -| macOS | `ln -s "/Applications/AutomataStudio.app/Contents/Resources/cli/automata" /usr/local/bin/automata` | -| Linux (.deb) | `ln -s /opt/AutomataStudio/resources/cli/automata /usr/local/bin/automata` | +| Windows | The installer adds `%LOCALAPPDATA%\Programs\AutomataStudio\resources\cli` to your user `PATH`, and the uninstaller removes it. Open a new terminal after installing. | +| macOS | In the app, choose **AutomataStudio → Install 'automata' Command in PATH**. It links `/usr/local/bin/automata`, asking for your password if that folder needs it. Move the app to Applications first. | +| Linux (.deb) | The package links `/usr/local/bin/automata`, and removing it takes the link away. An `automata` already there (from npm, say) is left alone. | +| Linux (AppImage) | Nothing to install: run `./AutomataStudio-*.AppImage --cli `. | | Linux (AppImage) | run `./AutomataStudio-*.AppImage --cli …` | On macOS and Linux, `AutomataStudio --cli …` works without the launcher too. @@ -207,13 +208,14 @@ This guide walks through it by task. Every option of every command is in the [co ## Install -**With the desktop app.** The CLI ships inside it and runs on the app's own executable, so nothing else is needed. The installer does not change your `PATH`; do it once: +**With the desktop app.** The CLI ships inside it and runs on the app's own executable, so nothing else is needed. The installers put `automata` on your `PATH`: -| Platform | Put this on your `PATH` | +| Platform | How `automata` gets on your `PATH` | | --- | --- | -| Windows | the folder `%LOCALAPPDATA%\Programs\AutomataStudio\resources\cli` (it holds `automata.cmd`) | -| macOS | `ln -s "/Applications/AutomataStudio.app/Contents/Resources/cli/automata" /usr/local/bin/automata` | -| Linux (.deb) | `ln -s /opt/AutomataStudio/resources/cli/automata /usr/local/bin/automata` | +| Windows | The installer adds `%LOCALAPPDATA%\Programs\AutomataStudio\resources\cli` to your user `PATH`, and the uninstaller removes it. Open a new terminal after installing. | +| macOS | In the app, choose **AutomataStudio → Install 'automata' Command in PATH**. It links `/usr/local/bin/automata`, asking for your password if that folder needs it. Move the app to Applications first. | +| Linux (.deb) | The package links `/usr/local/bin/automata`, and removing it takes the link away. An `automata` already there (from npm, say) is left alone. | +| Linux (AppImage) | Nothing to install: run `./AutomataStudio-*.AppImage --cli `. | | Linux (AppImage) | run `./AutomataStudio-*.AppImage --cli …` | On macOS and Linux, `AutomataStudio --cli …` works without the launcher too. @@ -594,6 +596,6 @@ Each tape symbol keeps one colour in every command — `play`, `trace`, and the **No colour** — output is piped, or `NO_COLOR` is set. `FORCE_COLOR=3` forces it. -**`automata` not found after installing the app** — the installer does not change `PATH`; see [Install](#install). +**`automata` not found after installing the app** — open a new terminal (one opened before the install keeps the old `PATH`); on macOS, use the app menu's Install command. See [Install](#install). **Solid / "SSR build" error when running the source directly** — run through `cli/automata.mjs` (it adds `--conditions=browser` for you) or use the bundle. diff --git a/electron/main.cjs b/electron/main.cjs index eaa8ef8..4cb1a31 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -588,6 +588,99 @@ function sendMenuAction(action) { if (mainWindow) mainWindow.webContents.send('menu-action', action); } +// The `automata` command line on a Mac's PATH. A .dmg is drag and drop and runs +// no install script, so the app offers it from its own menu, the way VS Code +// offers "Install 'code' command in PATH": a link in /usr/local/bin, which is on +// every shell's PATH, to the launcher inside the bundle. Windows and the .deb do +// this in their installers (build/installer.nsh, build/linux/after-install.sh). +const CLI_LINK = '/usr/local/bin/automata'; +const cliLauncher = () => path.join(process.resourcesPath, 'cli', 'automata'); + +// Whether the link is ours, someone else's, or not there at all. +function cliLinkState() { + let target; + try { target = fsSync.readlinkSync(CLI_LINK); } catch (e) { + return e.code === 'ENOENT' && !fsSync.existsSync(CLI_LINK) ? 'absent' : 'other'; + } + return target === cliLauncher() ? 'ours' : 'other'; +} + +// Run a shell command as an administrator, behind the system's password prompt. +// The command is passed to AppleScript as one string literal, escaped for it. +function runAsAdmin(command) { + const literal = '"' + command.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; + const r = require('node:child_process').spawnSync('osascript', ['-e', `do shell script ${literal} with administrator privileges`], { encoding: 'utf8' }); + if (r.status !== 0) { + // -128 is the user pressing Cancel on the password prompt: not an error. + if (/-128/.test(r.stderr)) return false; + throw new Error((r.stderr || '').trim() || `osascript exited ${r.status}`); + } + return true; +} + +const shellQuote = s => `'${String(s).replace(/'/g, `'\\''`)}'`; + +async function installCliCommand() { + const launcher = cliLauncher(); + // A copy run straight from the disk image or from Downloads is moved to a + // random read-only path by Gatekeeper (App Translocation); a link to it + // would break as soon as the app quit. + if (!app.isPackaged || /\/AppTranslocation\/|^\/Volumes\//.test(launcher)) { + await dialog.showMessageBox(mainWindow, { + type: 'info', + message: 'Move AutomataStudio to Applications first', + detail: 'The command line has to point at the app where it will stay. Drag AutomataStudio into Applications, open it from there, and choose this again.' + }); + return; + } + const state = cliLinkState(); + if (state === 'ours') { + await dialog.showMessageBox(mainWindow, { type: 'info', message: "'automata' is already installed", detail: `${CLI_LINK} points at this app. Open a terminal and run: automata --help` }); + return; + } + if (state === 'other') { + const { response } = await dialog.showMessageBox(mainWindow, { + type: 'warning', + buttons: ['Replace', 'Cancel'], + defaultId: 1, + cancelId: 1, + message: `${CLI_LINK} already exists`, + detail: 'Something else is installed under that name — perhaps automata from npm. Replace it with a link to this app?' + }); + if (response !== 0) return; + } + try { + // Try as the user first: on many Macs /usr/local/bin is theirs (Homebrew). + try { + fsSync.mkdirSync(path.dirname(CLI_LINK), { recursive: true }); + fsSync.rmSync(CLI_LINK, { force: true }); + fsSync.symlinkSync(launcher, CLI_LINK); + } catch (e) { + if (e.code !== 'EACCES' && e.code !== 'EPERM') throw e; + if (!runAsAdmin(`mkdir -p /usr/local/bin && ln -sfn ${shellQuote(launcher)} ${shellQuote(CLI_LINK)}`)) return; + } + await dialog.showMessageBox(mainWindow, { type: 'info', message: "'automata' is installed", detail: 'Open a new terminal and run: automata --help' }); + } catch (e) { + dialog.showErrorBox("Could not install 'automata'", `${e.message}\n\nYou can make the link yourself:\nsudo ln -sf "${launcher}" ${CLI_LINK}`); + } +} + +async function uninstallCliCommand() { + if (cliLinkState() !== 'ours') { + await dialog.showMessageBox(mainWindow, { type: 'info', message: "'automata' is not installed by this app", detail: `${CLI_LINK} is missing or points somewhere else, so it was left alone.` }); + return; + } + try { + try { fsSync.unlinkSync(CLI_LINK); } catch (e) { + if (e.code !== 'EACCES' && e.code !== 'EPERM') throw e; + if (!runAsAdmin(`rm -f ${shellQuote(CLI_LINK)}`)) return; + } + await dialog.showMessageBox(mainWindow, { type: 'info', message: "'automata' was removed from PATH" }); + } catch (e) { + dialog.showErrorBox("Could not remove 'automata'", e.message); + } +} + function buildMenu() { const isMac = process.platform === 'darwin'; @@ -597,6 +690,9 @@ function buildMenu() { submenu: [ { role: 'about' }, { type: 'separator' }, + { label: "Install 'automata' Command in PATH", click: () => installCliCommand() }, + { label: "Uninstall 'automata' Command from PATH", click: () => uninstallCliCommand() }, + { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, diff --git a/package.json b/package.json index d4bce68..7e34d7f 100644 --- a/package.json +++ b/package.json @@ -63,10 +63,21 @@ "win": { "target": [ "nsis" + ], + "extraResources": [ + { + "from": "build/win/cli-path.ps1", + "to": "cli-path.ps1" + } ] }, "nsis": { - "artifactName": "AutomataStudio-Setup-${version}.${ext}" + "artifactName": "AutomataStudio-Setup-${version}.${ext}", + "include": "build/installer.nsh" + }, + "deb": { + "afterInstall": "build/linux/after-install.sh", + "afterRemove": "build/linux/after-remove.sh" }, "appImage": { "artifactName": "AutomataStudio-${version}.${ext}"