Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
54 changes: 54 additions & 0 deletions .github/workflows/electron-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion build/cli/automata
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@
# → Contents/MacOS/AutomataStudio
# Linux <install>/resources/cli/automata → <install>/<executable>
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
Expand Down
34 changes: 34 additions & 0 deletions build/installer.nsh
Original file line number Diff line number Diff line change
@@ -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
75 changes: 75 additions & 0 deletions build/linux/after-install.sh
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions build/linux/after-remove.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash

# Delete the link to the binary
# update-alternatives --remove <name> <path>: '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
42 changes: 42 additions & 0 deletions build/win/cli-path.ps1
Original file line number Diff line number Diff line change
@@ -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()
}
24 changes: 13 additions & 11 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <command>`. |
| Linux (AppImage) | run `./AutomataStudio-*.AppImage --cli <command> …` |

On macOS and Linux, `AutomataStudio --cli <command> …` works without the launcher too.
Expand Down Expand Up @@ -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 <command>`. |
| Linux (AppImage) | run `./AutomataStudio-*.AppImage --cli <command> …` |

On macOS and Linux, `AutomataStudio --cli <command> …` works without the launcher too.
Expand Down Expand Up @@ -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.
Loading
Loading