Build a CLI executable with pear make
Turn a Bare command-line app into a standalone executable for macOS, Linux, and Windows with pear make, so users need no Node.js, Bare, or Pear to run it.
pear make packages a command-line app's JavaScript and its dependencies into one executable per OS and architecture. The executable runs on its own: users don't need Node.js, Bare, or the Pear CLI installed. Use it for CLIs, REPLs, TUIs, and other headless apps written for Bare, such as one started from the hello-pear-bare template.
Under the hood, pear make runs pear-make, a separate Pear CLI app that calls bare-build in standalone mode. It builds the executable and nothing else. To ship it over the air, you still assemble a deployment directory and stage it (see Where to go next). For an Electron desktop app, follow Build desktop distributables instead.
Requires Pear 3.7.0 or newer—run pear versions to check what you're on. Not available on 3.6.x or earlier.
Need the pear CLI? Install it from install.pears.com, or prefix any command below with npx. See Install & upgrade for details.
Before you begin
- Pear 3.7.0 or newer.
- A project written for Bare, with a
package.json. A Node.js app needs its built-ins swapped forbare-*modules first; see Migrate a Node.js app to Bare. - Its dependencies installed with
npm install.pear makebundles them fromnode_modules. - Network access on the first run, to install
pear-makefrom peers.
Point bin at your entry point
pear make builds the file named in the bin field of the package.json in the current directory. This guide builds a small greeting CLI:
{
"name": "hello-cli",
"version": "1.0.0",
"description": "Greet a peer from the terminal",
"bin": "bin.mjs",
"scripts": {
"start": "bare bin.mjs"
},
"dependencies": {
"bare-path": "^3.1.3",
"paparam": "^1.15.0"
},
"devDependencies": {
"bare": "^1.37.1"
}
}import { command, arg, flag, summary } from 'paparam'
import path from 'bare-path'
import pkg from './package.json'
// True under `bare bin.mjs`, false in the built executable
const isDev = path.basename(Bare.argv[0], path.extname(Bare.argv[0])) === 'bare'
const cmd = command(
pkg.name,
summary(pkg.description),
arg('[name]', 'Who to greet'),
flag('--shout|-s', 'Print the greeting in uppercase')
)
cmd.parse(Bare.argv.slice(isDev ? 2 : 1))
if (cmd.flags.help) Bare.exit()
const greeting = `Hello, ${cmd.args.name || 'peer'}!`
console.log(`${cmd.flags.shout ? greeting.toUpperCase() : greeting} (${pkg.name} v${pkg.version})`)The isDev check matters once the app is built. Under bare bin.mjs, the script path is the second argument, so the app's own arguments start at Bare.argv[2]. The built executable has no script path, so they start at Bare.argv[1].
Install the dependencies and run the app with Bare, to confirm it works before you build it:
npm installnpx bare bin.mjs Adabin has to be a single path. If yours is an object that maps several command names to files, or you want to build a file that bin doesn't name, pass the file as the argument instead, for example pear make src/cli.js.
Install pear-make
Print the pear-make version. On the first run this installs it:
pear make -vpear make looks for pear-make on your PATH. When it is missing, Pear asks before installing it:
⚠️ The pear-make CLI needs to be installed. To confirm type "INSTALL"
Install pear-make?Type INSTALL in uppercase. Anything else prints ✖ uppercase INSTALL to confirm and asks again. Pear then runs pear install for the pear-make link, which puts the binary in ~/.local/bin (%LOCALAPPDATA%\Programs\pear-make\ on Windows), and prints the version:
v1.1.0
SuccessThe first install downloads about 900 MB. After that, pear-make keeps itself up to date over the air, so you never reinstall it. Each run starts a short-lived background updater that logs to updates.log in pear-make's storage directory (~/Library/Application Support/pear-make on macOS). The updater keeps its own copy of the release there, so plan for about 1.6 GB of disk in total (pear-make 1.1.0 on macOS).
To install it without the prompt, run the install yourself once:
pear install pear://u3d888539k964t6omfzxdmf1a9c4jx4iiprm8jyt56x91izm78syIf the install directory (~/.local/bin, or %LOCALAPPDATA%\Programs\pear-make on Windows) wasn't on your PATH before the install, open a new terminal and run pear sidecar shutdown before you build. The sidecar starts pear-make with the PATH it inherited when it started, so until it restarts, builds fail with ✖ no such file or directory. Shutting it down also stops any pear seed running on this machine. An idle sidecar exits on its own about a minute after its last command finishes.
Build for your machine
Run pear make from the project root, with a filename and an output directory:
pear make --name hello-cli --path out/makeThe output is pear-make's own. The second line is the file it wrote:
pear-make v1.1.0
/path/to/hello-cli/out/make/hello-cli
Build finished.
SuccessAlways pass --name. As of pear-make 1.1.0, a build without it names the file app, not the name or productName from package.json.
--path defaults to the current directory, so without it the executable lands next to your package.json. out/make keeps builds in one place, matches the hello-pear-bare and hello-pear-qvac-tui templates, and is easy to add to .gitignore.
The name is turned into a safe filename: every run of characters other than letters and digits becomes a hyphen, a hyphen goes before a capital that follows a lowercase letter or digit, and the result is lowercased. So --name "Hello CLI" also writes hello-cli, and --name PearChat writes pear-chat. Windows builds add .exe. If your app updates itself through Pear OTA, the updater looks for by-arch/<host>/app/<name> in each release, using productName || name from package.json exactly as written. Use lowercase letters, digits, and hyphens for name and any productName, and pass the same value to --name, or the updater never finds the new binary. The hello-pear-daemon page has the full naming rules.
Run the executable
./out/make/hello-cli Ada --shoutHELLO, ADA! (hello-cli v1.0.0)The executable needs nothing else on the machine—no Node.js, Bare, or Pear—so you can copy it to another machine with the same OS and architecture and run it there. Sign it before you hand it to other people; see Sign the executable.
Build for other hosts
--host builds for an OS and architecture other than your own. It takes a <platform>-<arch> id, and the desktop ones are darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-arm64, and win32-x64. pear make takes one host per run, so loop over the ones you ship:
for host in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-arm64 win32-x64; do
pear make --host "$host" --name hello-cli --path "out/make/$host"
doneIn PowerShell:
foreach ($h in 'darwin-arm64','darwin-x64','linux-arm64','linux-x64','win32-arm64','win32-x64') {
pear make --host $h --name hello-cli --path "out/make/$h"
}Each host gets its own directory: out/make/linux-x64/hello-cli, out/make/win32-x64/hello-cli.exe, and so on. Give every host its own --path, because the macOS and Linux builds share a filename and would overwrite each other in one directory.
Build the darwin-* hosts on a Mac. bare-build adds the ad-hoc signature only when it runs on macOS, so a macOS executable built on Linux or Windows has no signature at all, and macOS on Apple silicon refuses to run an unsigned darwin-arm64 executable. If you cross-build anyway, sign the file on a Mac before you ship it, at least with codesign --sign - <file>.
Native addons cross-build too. bare-build bundles the prebuild for the target host from the package in node_modules, and the bare-* modules ship prebuilds for every desktop host.
Run it in CI or a script
Without a terminal, pear make can't ask before installing pear-make, so a missing pear-make fails the run:
✖ The pear-make CLI was not found. Please install using: pear install pear://u3d888539k964t6omfzxdmf1a9c4jx4iiprm8jyt56x91izm78syPass --install to install pear-make without asking, but only on a runner that doesn't have it yet. When a copy is already installed, pear install refuses to overwrite it, printing Refusing to overwrite existing: (Already installed: on Windows), and pear make exits with status 1. Don't combine --install with --json either: in Pear 3.7.0 the install's progress lines go into the JSON stream as plain text.
On a runner that keeps its home directory between jobs, install pear-make only when it is missing. In a bash step:
command -v pear-make >/dev/null || pear install pear://u3d888539k964t6omfzxdmf1a9c4jx4iiprm8jyt56x91izm78sy
pear make --no-install --name hello-cli --path out/makeIn PowerShell, the default shell on Windows runners:
if (-not (Get-Command pear-make -ErrorAction SilentlyContinue)) { pear install pear://u3d888539k964t6omfzxdmf1a9c4jx4iiprm8jyt56x91izm78sy }
pear make --no-install --name hello-cli --path out/makePut the install directory on the job's PATH before the first pear command, because that command starts the sidecar (see the warning in Install pear-make). It is ~/.local/bin on macOS and Linux, and %LOCALAPPDATA%\Programs\pear-make on Windows, where pear install adds it only to the user PATH in the registry, which a running job doesn't read. On GitHub Actions, append it in an earlier step: echo "$HOME/.local/bin" >> "$GITHUB_PATH" in bash, or "$env:LOCALAPPDATA\Programs\pear-make" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append in PowerShell.
For machine-readable output, pass --json. Each piece of pear-make output arrives as a make object, and the stream ends with a final object or, on failure, an error object:
{"cmd":"make","tag":"make","data":{"chunk":"pear-make v1.1.0\n"}}
{"cmd":"make","tag":"make","data":{"chunk":"/path/to/hello-cli/out/make/hello-cli\n\nBuild finished.\n\n"}}
{"cmd":"make","tag":"final","data":{"success":true}}In Pear 3.7.0 the objects are written back to back, with no newline between them (they're shown one per line here). jq reads them as they are, but tools that split on lines don't. To print the build log:
set -o pipefail
pear make --json --name hello-cli --path out/make | jq -rj 'if .tag == "make" then .data.chunk elif .tag == "error" then .data.message + "\n" else empty end'A failed build exits with status 1. set -o pipefail keeps that status through the pipe, which the default run shell on GitHub Actions doesn't do, and the filter also prints the message of an error object, such as the ENOENT one Pear emits when it can't start pear-make.
Sign the executable
pear make doesn't sign for distribution. A macOS executable built on a Mac carries only an ad-hoc signature (codesign -dv reports Signature=adhoc), which Gatekeeper blocks when users download it through a browser, and one built on Linux or Windows has no signature at all. The Windows executable has no Authenticode signature.
Sign and notarize the executable before you hand it to users. pear make has no signing options, so either sign the file it wrote with your platform's tools, or build with bare-build --standalone directly and use its signing flags. The npm run make of the hello-pear-bare and hello-pear-qvac-tui templates takes the second route, with the MAC_CODESIGN_IDENTITY, KEYCHAIN_PROFILE, and WINDOWS_CERT_SHA1 variables (see Build a standalone binary). To notarize a macOS executable you sign yourself, sign it with the hardened runtime (codesign --options runtime) and the com.apple.security.cs.allow-jit and com.apple.security.cs.allow-unsigned-executable-memory entitlements, the ones bare-build adds when it signs with the hardened runtime. In CI, the make-pear-app action provides the same credentials.
Troubleshooting
✖ package.json not found in <dir>.Runpear makefrom the project root, or pass the entry file as the argument.✖ bin field not found in package.jsonAdd abinfield that points at your entry file, or pass the entry file as the argument.✖ no such file or directoryThe sidecar can't findpear-makeon itsPATH. This happens whenpear-makewas installed, or its directory added toPATH, after the sidecar started, and when--no-installis passed with nopear-makeinstalled. Check thatcommand -v pear-make(Get-Command pear-makein PowerShell) finds it, then runpear sidecar shutdown, which also stops any runningpear seed, and build again.✖ pear-make failed with exit code <n>pear-makeitself failed, and the lines before this one are its error.MODULE_NOT_FOUND: Cannot find module 'file:///…'means the entry file doesn't exist.Unknown host '<host>'meanspear-makedoesn't recognize the--hostid.Refusing to overwrite existing:(Already installed:on Windows) You passed--installwithpear-makealready installed. Drop--install.- The executable is called
app. Pass--name; see Build for your machine.
Where to go next
- Deploy your application: set the
upgradelink, then assemble your executables into a deployment directory withpear build, stage, provision, and seed. Users then install withpear install pear://<key>. - Start a terminal app from the hello-pear-bare template and Start a short-lived CLI from the hello-pear-daemon template: CLI apps that already update themselves over the air.
- Bundle a Bare app: what
bare-builddoes underneath, and the optionspear makedoesn't expose. pear makereference: every flag and error.
Last updated on