Skip to content

balena push fails on Windows when CLI is installed at a path containing spaces (e.g. C:\Program Files\balena-cli\) #3091

Description

@schubydoo

Note: This is ovbiously written with AI after a lot of debugging. I generally write these myself, but it's been a long day. I went with manually patching compose.js:27 as a workaround which works correctly on my setup.

Summary

@balena/compose-parser@0.2.0 (shipped with balena-cli@25.1.0) spawns balena-compose-parser.exe via a shell exec() call with the binary path interpolated unquoted. On Windows installs whose path contains a space (notably the default C:\Program Files\balena-cli\ location used by the official installer), cmd.exe parses the path with the space as a separator and fails to find the command. The error then surfaces deep in the stack as Unexpected end of JSON input.

This breaks any command that parses the compose file — balena push, balena deploy, etc.

Reproduction

  1. Install balena-cli@25.1.0 to C:\Program Files\balena-cli\ (the default for the Windows installer).
  2. Run balena push <fleet> from a project that has a docker-compose.yml.

Expected

The compose file is parsed and the build is uploaded.

Actual

Could not parse stderr line as JSON: 'C:\Program' is not recognized as an internal or external command,
Could not parse stderr line as JSON: operable program or batch file.
Error parsing composition file "C:\path\to\project\docker-compose.yml":
Unexpected end of JSON input

SyntaxError: Error parsing composition file "...":
Unexpected end of JSON input
    at JSON.parse (<anonymous>)
    at Object.parse (C:\Program Files\balena-cli\client\node_modules\@balena\compose-parser\dist\compose.js:43:31)
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
    at async parseComposePaths (C:\Program Files\balena-cli\client\build\utils\compose_ts.js:66:16)
    at async getServiceDirsFromComposition (C:\Program Files\balena-cli\client\build\utils\compose_ts.js:358:27)
    at async tarDirectory (C:\Program Files\balena-cli\client\build\utils\compose_ts.js:389:25)
    at async getTarStream (C:\Program Files\balena-cli\client\build\utils\remote-build.js:210:27)
    at async getRemoteBuildStream (C:\Program Files\balena-cli\client\build\utils\remote-build.js:274:23)
    at async Object.startRemoteBuild (C:\Program Files\balena-cli\client\build\utils\remote-build.js:39:36)
    at async PushCmd.pushToCloud (C:\Program Files\balena-cli\client\build\commands\push\index.js:78:27)

The 'C:\Program' is not recognized as an internal or external command line is cmd.exe trying to execute the unquoted path with a space.

Root cause

In @balena/compose-parser/dist/compose.js:

const binaryName = process.platform === 'win32'
    ? 'balena-compose-parser.exe'
    : 'balena-compose-parser';
const binaryPath = path.join(__dirname, '..', 'bin', binaryName);
const result = await exec(`${binaryPath} ${fileFlags} ${projectName}`, {
    env: process.env,
});

binaryPath resolves to e.g. C:\Program Files\balena-cli\client\node_modules\@balena\compose-parser\bin\balena-compose-parser.exe. It's interpolated raw into a child_process.exec() shell command, so cmd /c sees:

C:\Program Files\balena-cli\...\balena-compose-parser.exe -f ... <uuid>

and parses C:\Program as the command name. The subprocess never starts, stdout is empty, JSON.parse('') throws Unexpected end of JSON input at line 43 of the same file.

The bug is independent of the project's docker-compose.yml content — the file is never read by the failing process.

Suggested fix

Two options, in order of preference:

1. Use execFile instead of exec — avoids shell quoting entirely:

const { execFile } = require('child_process');
const execFileP = promisify(execFile);
// ...
const args = [...filePaths.flatMap(p => ['-f', p]), projectName];
const result = await execFileP(binaryPath, args, { env: process.env });

2. Quote binaryPath in the shell command — minimal change:

-const result = await exec(`${binaryPath} ${fileFlags} ${projectName}`, {
+const result = await exec(`"${binaryPath}" ${fileFlags} ${projectName}`, {

Note that ${fileFlags} (which contains user-supplied compose paths) is also unquoted and will hit the same issue if a user's project lives under a path with spaces (e.g. C:\Users\Some Name\project\). A robust fix should quote those too, or better, switch to execFile and pass arguments as an array.

Workaround

Manually patch compose.js:27 per option 2 above, OR reinstall balena-cli to a path without spaces (e.g. C:\balena-cli\).

Environment

  • balena-cli: 25.1.0 (win32-x64, bundled Node v24.14.1)
  • @balena/compose-parser: 0.2.0
  • OS: Windows 11, build 10.0.26200.8313
  • Install location: C:\Program Files\balena-cli\ (default for the official Windows installer)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions