nodalis-compiler 1.0.22 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # Changelog
2
2
 
3
- ## [1.0.22] 2026-03-03
3
+ ## [1.0.23] 2026-03-03
4
4
  - Changed IEC parser to interpret Function Blocks that are actually standard functions to a formal function call.
5
5
  - Fixed nodejs/jint compiles to put executables in a bin folder.
6
6
  - Fixed syntax errors with repeat.
7
7
  - Prevent stale files in ST bundle compile.
8
+ - Added a new "action" for "get-toolchains", which will download the necessary toolchains for CPP and Arduino.
8
9
 
9
10
  ## [1.0.17] 2026-02-25
10
11
  - Added support for compilation of multiple ST files as a single project.
package/README.md CHANGED
@@ -56,6 +56,7 @@ Usage:
56
56
  Actions:
57
57
  --action list-compilers
58
58
  --action compile
59
+ --action get-toolchains
59
60
  ```
60
61
 
61
62
  ---
@@ -68,6 +69,18 @@ Actions:
68
69
  nodalis --action list-compilers
69
70
  ```
70
71
 
72
+ ### ✔ Install managed C/C++ toolchains
73
+
74
+ ```bash
75
+ nodalis --action get-toolchains
76
+ ```
77
+
78
+ This installs:
79
+ - Managed Zig-based C/C++ cross-compilers
80
+ - Managed `arduino-cli`
81
+ - Default Arduino board cores for the built-in Nodalis FQBN targets
82
+ - Required Arduino libraries for the built-in Nodalis Arduino runtime, including `ArduinoModbus`
83
+
71
84
  ---
72
85
 
73
86
  ### ✔ Compile a Structured Text program
@@ -113,23 +126,34 @@ await app.compile({
113
126
 
114
127
  #### Dependencies
115
128
 
116
- - Uses a default cross-compiler profile tuned for macOS-style Clang/LLVM toolchains when no overrides are provided.
129
+ - Uses managed Zig-based toolchain wrappers under `~/.nodalis/toolchains` when no overrides are provided.
130
+ - Install those toolchains with `nodalis --action get-toolchains`.
131
+ - `get-toolchains` also installs a managed `arduino-cli` plus the default Nodalis Arduino board cores.
132
+ - `get-toolchains` also installs the default Arduino libraries required by the built-in runtime support.
133
+ - On Windows hosts, the managed toolchain installs wrappers for all Linux and Windows targets.
134
+ - On macOS hosts, the managed toolchain installs wrappers for all Linux, Windows, and macOS targets.
117
135
  - Supply a `toolchain.json` file beside your source to describe a custom toolchain. Example:
118
136
 
119
137
  ```json
120
138
  {
121
- "linux-arm": "arm-linux-gnueabi-g++",
122
- "linux-arm64": "aarch64-linux-gnu-g++",
123
- "linux-x64": "x86_64-linux-gnu-g++",
124
- "macos-arm64": "clang++",
125
- "macos-x64": "clang++",
126
- "windows-x64": "x86_64-w64-mingw32-g++",
127
- "windows-arm64": "/opt/llvm-mingw/bin/aarch64-w64-mingw32-g++"
139
+ "linux-arm": "/Users/you/.nodalis/toolchains/zig/0.15.2/wrappers/zig-linux-arm-c++",
140
+ "linux-arm64": "/Users/you/.nodalis/toolchains/zig/0.15.2/wrappers/zig-linux-arm64-c++",
141
+ "linux-x64": "/Users/you/.nodalis/toolchains/zig/0.15.2/wrappers/zig-linux-x64-c++",
142
+ "windows-x64": "/Users/you/.nodalis/toolchains/zig/0.15.2/wrappers/zig-windows-x64-c++",
143
+ "windows-arm64": "/Users/you/.nodalis/toolchains/zig/0.15.2/wrappers/zig-windows-arm64-c++",
144
+ "macos-x64": "/Users/you/.nodalis/toolchains/zig/0.15.2/wrappers/zig-macos-x64-c++",
145
+ "macos-arm64": "/Users/you/.nodalis/toolchains/zig/0.15.2/wrappers/zig-macos-arm64-c++"
128
146
  }
129
147
  ```
130
148
 
131
- - Without an explicit file Nodalis falls back to the default compiler for the host OS (`clang++` on macOS, `g++` on Linux, and `cl.exe` or MinGW-w64 `g++` on Windows).
132
- - Common cross-compiler sources: Homebrew packages (`brew install armmbed/formulae/arm-none-eabi-gcc`) and osxcross for macOS targeting, MinGW-w64/MSYS2 or Visual Studio Build Tools for Windows, and distro packages such as `gcc-arm-linux-gnueabihf` or `x86_64-w64-mingw32-g++` on Linux.
149
+ - The managed installer skips macOS targets on non-macOS hosts.
150
+ - `toolchain.json` remains the escape hatch for unsupported or custom compiler setups.
151
+
152
+ ### Arduino
153
+
154
+ Nodalis uses a managed `arduino-cli` by default when present under `~/.nodalis/toolchains/arduino-cli`.
155
+ Running `nodalis --action get-toolchains` also installs the default board cores required by the built-in Arduino FQBN targets, currently including `arduino:mbed_opta:opta`.
156
+ It also installs the default Arduino libraries required by the shipped support code, currently including `ArduinoModbus`.
133
157
 
134
158
  #### Variations
135
159
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodalis-compiler",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "description": "Compiles IEC-61131-3/10 languages into code that can be used as a PLC on multiple platforms.",
5
5
  "icon": "nodalis.png",
6
6
  "main": "src/nodalis.js",
@@ -23,14 +23,12 @@ import { Compiler, IECLanguage, OutputType, CommunicationProtocol } from './Comp
23
23
  import * as iec from './iec-parser/parser.js';
24
24
  import { parseStructuredText } from './st-parser/parser.js';
25
25
  import { transpile } from './st-parser/gcctranspiler.js';
26
+ import { DEFAULT_ARDUINO_FQBN } from './arduinoDefaults.js';
27
+ import { getManagedArduinoCliPath, getManagedArduinoCliExecOptions } from '../toolchains.js';
26
28
 
27
29
  const __filename = fileURLToPath(import.meta.url);
28
30
  const __dirname = dirname(__filename);
29
31
 
30
- const DEFAULT_ARDUINO_FQBN = {
31
- 'arduino-opta': 'arduino:mbed_opta:opta'
32
- };
33
-
34
32
  const MODBUS_ARDUINO_CORE_IDS = new Set([
35
33
  'arduino:megaavr',
36
34
  'arduino:samd',
@@ -257,7 +255,7 @@ void loop() {
257
255
  fs.cpSync(path.join(supportDir, 'json.hpp'), path.join(outputPath, 'json.hpp'), { force: true });
258
256
 
259
257
  if (this.isExecutableOutput()) {
260
- const arduinoCli = compilerConfig.arduino_cli || compilerConfig.arduinoCli || 'arduino-cli';
258
+ const arduinoCli = compilerConfig.arduino_cli || compilerConfig.arduinoCli || getManagedArduinoCliPath();
261
259
  const arduinoFqbn = this.resolveArduinoFqbn(target, compilerConfig);
262
260
  if (!arduinoFqbn) {
263
261
  throw new Error(`No Arduino FQBN configured for target "${target}". Use an explicit FQBN target (e.g. arduino:mbed_opta:opta) or add "arduino_fqbn" to toolchain.json.`);
@@ -272,14 +270,18 @@ void loop() {
272
270
  fs.mkdirSync(binDir, { recursive: true });
273
271
  const arduinoCompileCmd = `${arduinoCli} compile --fqbn "${arduinoFqbn}" "${outputPath}" --build-path "${buildDir}" --output-dir "${binDir}" --export-binaries`;
274
272
  try {
275
- execSync(arduinoCompileCmd, { stdio: 'pipe' });
273
+ execSync(arduinoCompileCmd, getManagedArduinoCliExecOptions({ stdio: 'pipe' }));
276
274
  } catch (err) {
277
275
  const stderrText = getExecOutputText(err?.stderr);
278
276
  const stdoutText = getExecOutputText(err?.stdout);
279
277
  const compilerOutput = [stderrText, stdoutText].filter(Boolean).join('\n');
278
+ const missingArduinoLibrary = compilerOutput.includes('ArduinoModbus.h') || compilerOutput.includes('No such file or directory');
280
279
  const details = compilerOutput || err.message;
281
280
  throw new Error(
282
- `Arduino CLI build failed for "${arduinoFqbn}". Verify arduino-cli and board core availability.\n${details}`
281
+ `Arduino CLI build failed for "${arduinoFqbn}". ` +
282
+ `${missingArduinoLibrary
283
+ ? 'Run "nodalis --action get-toolchains" to install the managed Arduino cores and libraries.'
284
+ : 'Verify arduino-cli, board core, and library availability.'}\n${details}`
283
285
  );
284
286
  }
285
287
  }
@@ -353,7 +355,7 @@ void loop() {
353
355
 
354
356
  let coreList = '';
355
357
  try {
356
- coreList = execSync(`${arduinoCli} core list`, { encoding: 'utf8' });
358
+ coreList = execSync(`${arduinoCli} core list`, getManagedArduinoCliExecOptions({ encoding: 'utf8' }));
357
359
  } catch (err) {
358
360
  throw new Error(`Failed to query installed Arduino cores using "${arduinoCli}". ${err.message}`);
359
361
  }
@@ -362,8 +364,8 @@ void loop() {
362
364
  }
363
365
 
364
366
  try {
365
- execSync(`${arduinoCli} core update-index`, { stdio: 'inherit' });
366
- execSync(`${arduinoCli} core install ${coreId}`, { stdio: 'inherit' });
367
+ execSync(`${arduinoCli} core update-index`, getManagedArduinoCliExecOptions({ stdio: 'inherit' }));
368
+ execSync(`${arduinoCli} core install ${coreId}`, getManagedArduinoCliExecOptions({ stdio: 'inherit' }));
367
369
  } catch (err) {
368
370
  throw new Error(`Failed to install Arduino core "${coreId}" required for ${arduinoFqbn}. ${err.message}`);
369
371
  }
@@ -387,7 +389,10 @@ void loop() {
387
389
  const targets = Object.keys(DEFAULT_ARDUINO_FQBN);
388
390
 
389
391
  try {
390
- const boardListRaw = execSync('arduino-cli board listall --format json', { encoding: 'utf8' });
392
+ const boardListRaw = execSync(
393
+ `${getManagedArduinoCliPath()} board listall --format json`,
394
+ getManagedArduinoCliExecOptions({ encoding: 'utf8' })
395
+ );
391
396
  const boardList = JSON.parse(boardListRaw);
392
397
  const boards = Array.isArray(boardList.boards) ? boardList.boards : [];
393
398
 
@@ -24,19 +24,12 @@ import { parseStructuredText } from './st-parser/parser.js';
24
24
  import { transpile } from './st-parser/gcctranspiler.js';
25
25
  import { fileURLToPath } from 'node:url';
26
26
  import { dirname } from 'node:path';
27
+ import { getDefaultToolchain, getToolchainRoot, isManagedZigCompiler } from '../toolchains.js';
27
28
 
28
29
  const __filename = fileURLToPath(import.meta.url);
29
30
  const __dirname = dirname(__filename);
30
31
 
31
- const DEFAULT_TOOLCHAIN = {
32
- "linux-arm": "arm-linux-gnueabi-g++",
33
- "linux-arm64": "aarch64-linux-gnu-g++",
34
- "linux-x64": "x86_64-linux-gnu-g++",
35
- "macos-arm64": "clang++",
36
- "macos-x64": "clang++",
37
- "windows-x64": "x86_64-w64-mingw32-g++",
38
- "windows-arm64": "/opt/llvm-mingw/bin/aarch64-w64-mingw32-g++"
39
- };
32
+ const DEFAULT_TOOLCHAIN = getDefaultToolchain();
40
33
 
41
34
  let ToolChain = { ...DEFAULT_TOOLCHAIN };
42
35
 
@@ -79,7 +72,7 @@ export class CPPCompiler extends Compiler {
79
72
  const isStructuredTextLanguage = String(language || '').toUpperCase() === IECLanguage.STRUCTURED_TEXT;
80
73
  const directoryBundleMode = sourceIsDirectory && isStructuredTextLanguage && typeof resourceName === 'string' && resourceName.trim().length > 0;
81
74
 
82
- ToolChain = { ...DEFAULT_TOOLCHAIN };
75
+ ToolChain = { ...getDefaultToolchain() };
83
76
  const sourceDir = sourceIsDirectory ? sourcePath : path.dirname(sourcePath);
84
77
  const toolchainConfigPath = path.join(sourceDir, "toolchain.json");
85
78
  if (fs.existsSync(toolchainConfigPath)) {
@@ -369,7 +362,12 @@ int main() {
369
362
  .forEach((dir) => searchDirs.add(dir));
370
363
  }
371
364
 
372
- const compilerPath = execSync(`which "${compiler}"`, { encoding: 'utf8' }).trim();
365
+ const compilerPath = path.isAbsolute(compiler)
366
+ ? compiler
367
+ : execSync(process.platform === 'win32' ? `where "${compiler}"` : `which "${compiler}"`, { encoding: 'utf8' })
368
+ .split(/\r?\n/)
369
+ .map((line) => line.trim())
370
+ .find(Boolean);
373
371
  if (compilerPath) {
374
372
  const compilerDir = path.dirname(compilerPath);
375
373
  searchDirs.add(compilerDir);
@@ -500,44 +498,28 @@ int main() {
500
498
  }
501
499
 
502
500
  detectCompiler(hostOs, hostArch, targetOs, targetArch) {
503
- const hostDefaults = {
504
- linux: "g++",
505
- macos: "clang++",
506
- windows: "cl.exe"
507
- };
508
- const hostKey = `${hostOs}-${hostArch}`;
509
501
  const targetKey = `${targetOs}-${targetArch}`;
510
502
 
511
503
  const ensureCompilerAvailable = (compilerName, message) => {
512
- const versionCommand = compilerName === "cl.exe" ? compilerName : `${compilerName} --version`;
513
504
  try {
514
- execSync(versionCommand, { stdio: "ignore" });
505
+ execSync(`"${compilerName}" --version`, { stdio: 'ignore' });
515
506
  } catch {
516
507
  throw new Error(message);
517
508
  }
518
509
  };
519
510
 
520
- if (targetKey === hostKey) {
521
- const defaultCompiler = hostDefaults[hostOs];
522
- if (!defaultCompiler) {
523
- throw new Error(`No default compiler configured for host platform ${hostOs}.`);
524
- }
525
- ensureCompilerAvailable(
526
- defaultCompiler,
527
- `The default compiler "${defaultCompiler}" is not available. Install it using your package manager (e.g., brew install ${defaultCompiler} or apt install ${defaultCompiler}).
528
- You can also create a file called "toolchain.json" in your source directory which will supply the path to the gnu c compiler for each platform. See the README file for more details.`
529
- );
530
- return defaultCompiler;
531
- }
532
-
533
511
  const configuredCompiler = ToolChain[targetKey];
534
512
  if (!configuredCompiler) {
535
- throw new Error(`No cross-compiler is configured for target ${targetKey}. Add it to toolchain.json or update the ToolChain defaults.`);
513
+ throw new Error(
514
+ `No toolchain is configured for target ${targetKey} on host ${hostOs}-${hostArch}. ` +
515
+ `Run "nodalis --action get-toolchains" or add an override in toolchain.json.`
516
+ );
536
517
  }
537
518
  ensureCompilerAvailable(
538
519
  configuredCompiler,
539
- `Cross-compiler "${configuredCompiler}" for target ${targetKey} is not available. Install it via your package manager (e.g., brew install ${configuredCompiler} or apt install ${configuredCompiler}).
540
- You can also create a file called "toolchain.json" in your source directory which will supply the path to the gnu c compiler for each platform. See the README file for more details.`
520
+ `Toolchain "${configuredCompiler}" for target ${targetKey} is not available. ` +
521
+ `Run "nodalis --action get-toolchains" to install managed toolchains under ${getToolchainRoot()}, ` +
522
+ `or add a custom compiler path in toolchain.json.`
541
523
  );
542
524
  return configuredCompiler;
543
525
  }
@@ -561,7 +543,7 @@ int main() {
561
543
  if (compiler === 'cl.exe') {
562
544
  return { c: [], cpp: [] };
563
545
  }
564
- let flags = { //default flags are for macos clang
546
+ let flags = { // default flags are for managed/native clang wrappers
565
547
  'linux-x64': [],
566
548
  'linux-arm64': [],
567
549
  'linux-arm': [],
@@ -581,7 +563,18 @@ int main() {
581
563
  "macos-arm64": "",
582
564
  }
583
565
 
584
- if (!compiler.includes("clang")) {
566
+ if (isManagedZigCompiler(compiler)) {
567
+ flags = {
568
+ 'linux-x64': [],
569
+ 'linux-arm64': [],
570
+ 'linux-arm': [],
571
+ 'macos-x64': [],
572
+ 'macos-arm64': [],
573
+ 'windows-x64': [],
574
+ 'windows-arm64': []
575
+ };
576
+ }
577
+ else if (!compiler.includes("clang")) {
585
578
  flags = {
586
579
  'linux-x64': ["-D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE -pthread"],
587
580
  'linux-arm64': ["-D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE -pthread"],
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_ARDUINO_FQBN = {
2
+ 'arduino-opta': 'arduino:mbed_opta:opta'
3
+ };
4
+
5
+ export const DEFAULT_ARDUINO_FQBNS = Object.values(DEFAULT_ARDUINO_FQBN);
@@ -671,19 +671,12 @@ template <typename T, typename... Ts>
671
671
  inline T MUX(std::size_t K, T in0, Ts... rest)
672
672
  {
673
673
  constexpr std::size_t N = 1 + sizeof...(Ts);
674
+ T values[N] = {in0, static_cast<T>(rest)...};
674
675
  if (K >= N)
675
676
  {
676
- throw std::out_of_range("MUX selector out of range");
677
+ return values[0];
677
678
  }
678
-
679
- auto values = std::tuple<T, Ts...>(in0, rest...);
680
- return std::apply(
681
- [K](auto... elems) -> T
682
- {
683
- T arr[] = {elems...};
684
- return arr[K];
685
- },
686
- values);
679
+ return values[K];
687
680
  };
688
681
 
689
682
  // ============================================================
package/src/nodalis.js CHANGED
@@ -30,6 +30,7 @@ import { FileProgrammer } from './programmers/FileProgrammer.js';
30
30
  import { SSHProgrammer } from './programmers/SSHProgrammer.js';
31
31
  import { ArduinoProgrammer } from './programmers/ArduinoProgrammer.js';
32
32
  import { CompileList } from "mticp-npm"
33
+ import { getToolchainRoot, installDefaultToolchains } from './toolchains.js';
33
34
 
34
35
  const __filename = fileURLToPath(import.meta.url);
35
36
  const __dirname = path.dirname(__filename);
@@ -172,6 +173,10 @@ export class Nodalis {
172
173
  }
173
174
  }
174
175
 
176
+ async getToolchains() {
177
+ return installDefaultToolchains();
178
+ }
179
+
175
180
  }
176
181
 
177
182
  function isCliEntryPoint() {
@@ -221,6 +226,11 @@ Actions:
221
226
  --sshPort Optional SSH port for SSH deployment.
222
227
  --arduinoFqbn Required for Arduino target if --target Arduino.
223
228
 
229
+ --action get-toolchains
230
+ Detects the host OS/arch and installs managed C/C++ toolchains,
231
+ arduino-cli, and default Arduino board cores under:
232
+ ${getToolchainRoot()}
233
+
224
234
  Examples:
225
235
  node nodalis.js --action list-compilers
226
236
 
@@ -231,6 +241,8 @@ Examples:
231
241
  --resourceName MyPLC \\
232
242
  --sourcePath ./examples/pump.iec \\
233
243
  --language st
244
+
245
+ node nodalis.js --action get-toolchains
234
246
  `);
235
247
  process.exit(0);
236
248
  }
@@ -296,9 +308,18 @@ Examples:
296
308
  break;
297
309
  }
298
310
 
311
+ case 'get-toolchains': {
312
+ app.getToolchains().then((result) => {
313
+ console.log(JSON.stringify(result, null, 2));
314
+ }).catch(err => {
315
+ console.error(`Toolchain installation failed: ${err.message}`);
316
+ });
317
+ break;
318
+ }
319
+
299
320
  default: {
300
321
  console.error(`Unknown or missing action: ${argMap.action}`);
301
- console.error(`Valid actions: list-compilers, list-programmers, compile, deploy`);
322
+ console.error(`Valid actions: list-compilers, list-programmers, compile, deploy, get-toolchains`);
302
323
  break;
303
324
  }
304
325
  }
@@ -16,6 +16,7 @@ import fs from 'fs/promises';
16
16
  import path from 'path';
17
17
  import { Programmer } from './Programmer.js';
18
18
  import { runCommand } from './utils.js';
19
+ import { getManagedArduinoCliExecOptions, getManagedArduinoCliPath } from '../toolchains.js';
19
20
 
20
21
  export class ArduinoProgrammer extends Programmer {
21
22
  constructor(options) {
@@ -58,7 +59,11 @@ export class ArduinoProgrammer extends Programmer {
58
59
  }
59
60
  }
60
61
 
61
- const { stdout, stderr } = await runCommand('arduino-cli', args);
62
+ const { stdout, stderr } = await runCommand(
63
+ getManagedArduinoCliPath(),
64
+ args,
65
+ getManagedArduinoCliExecOptions()
66
+ );
62
67
  if (stdout) {
63
68
  console.log(stdout.trim());
64
69
  }
@@ -0,0 +1,417 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import https from 'https';
5
+ import { execFileSync } from 'child_process';
6
+ import { pipeline } from 'stream/promises';
7
+ import { DEFAULT_ARDUINO_FQBNS } from './compilers/arduinoDefaults.js';
8
+
9
+ const ZIG_VERSION = '0.15.2';
10
+ const ARDUINO_CLI_VERSION = '1.3.1';
11
+ const TOOLCHAIN_ROOT = path.join(os.homedir(), '.nodalis', 'toolchains');
12
+ const WRAPPER_EXTENSION = process.platform === 'win32' ? '.cmd' : '';
13
+ const DEFAULT_ARDUINO_LIBRARIES = ['ArduinoModbus'];
14
+
15
+ const TARGET_TRIPLES = {
16
+ 'linux-arm': 'arm-linux-gnueabihf',
17
+ 'linux-arm64': 'aarch64-linux-gnu',
18
+ 'linux-x64': 'x86_64-linux-gnu',
19
+ 'macos-arm64': 'aarch64-macos',
20
+ 'macos-x64': 'x86_64-macos',
21
+ 'windows-arm64': 'aarch64-windows-gnu',
22
+ 'windows-x64': 'x86_64-windows-gnu'
23
+ };
24
+
25
+ const normalizeHostOS = (value) => {
26
+ if (value === 'win32') return 'windows';
27
+ if (value === 'darwin') return 'macos';
28
+ if (value === 'linux') return 'linux';
29
+ throw new Error(`Unsupported host operating system: ${value}`);
30
+ };
31
+
32
+ const normalizeHostArch = (value) => {
33
+ if (value === 'x64') return 'x64';
34
+ if (value === 'arm64') return 'arm64';
35
+ if (value.startsWith('arm')) return 'arm';
36
+ throw new Error(`Unsupported host architecture: ${value}`);
37
+ };
38
+
39
+ const stripArchiveExtension = (archiveName) => {
40
+ if (archiveName.endsWith('.tar.gz')) return archiveName.slice(0, -7);
41
+ if (archiveName.endsWith('.tar.xz')) return archiveName.slice(0, -7);
42
+ if (archiveName.endsWith('.zip')) return archiveName.slice(0, -4);
43
+ return archiveName;
44
+ };
45
+
46
+ const getWrapperDir = () => path.join(TOOLCHAIN_ROOT, 'zig', ZIG_VERSION, 'wrappers');
47
+
48
+ const getWrapperPath = (target) => path.join(getWrapperDir(), `zig-${target}-c++${WRAPPER_EXTENSION}`);
49
+
50
+ const getArduinoCliRootDir = () => path.join(TOOLCHAIN_ROOT, 'arduino-cli', ARDUINO_CLI_VERSION);
51
+
52
+ const getArduinoCliDataDir = () => path.join(getArduinoCliRootDir(), 'data');
53
+
54
+ const getArduinoCliDownloadsDir = () => path.join(getArduinoCliRootDir(), 'downloads');
55
+
56
+ const getArduinoCliUserDir = () => path.join(getArduinoCliRootDir(), 'user');
57
+
58
+ const getZigInstallDir = (host = detectHostPlatform()) => {
59
+ const archiveName = getZigArchiveName(host);
60
+ return path.join(TOOLCHAIN_ROOT, 'zig', ZIG_VERSION, stripArchiveExtension(archiveName));
61
+ };
62
+
63
+ const getZigBinaryPath = (host = detectHostPlatform()) => {
64
+ const installDir = getZigInstallDir(host);
65
+ return path.join(installDir, process.platform === 'win32' ? 'zig.exe' : 'zig');
66
+ };
67
+
68
+ const getArduinoCliArchiveName = (host = detectHostPlatform()) => {
69
+ if (host.os === 'windows') {
70
+ const archSegment = host.arch === 'arm64' ? 'Windows_ARM64' : host.arch === 'x64' ? 'Windows_64bit' : null;
71
+ if (!archSegment) {
72
+ throw new Error(`Unsupported Windows host architecture for arduino-cli: ${host.arch}`);
73
+ }
74
+ return `arduino-cli_${ARDUINO_CLI_VERSION}_${archSegment}.zip`;
75
+ }
76
+
77
+ if (host.os === 'macos') {
78
+ const archSegment = host.arch === 'arm64' ? 'macOS_ARM64' : host.arch === 'x64' ? 'macOS_64bit' : null;
79
+ if (!archSegment) {
80
+ throw new Error(`Unsupported macOS host architecture for arduino-cli: ${host.arch}`);
81
+ }
82
+ return `arduino-cli_${ARDUINO_CLI_VERSION}_${archSegment}.tar.gz`;
83
+ }
84
+
85
+ if (host.os === 'linux') {
86
+ if (host.arch === 'x64') return `arduino-cli_${ARDUINO_CLI_VERSION}_Linux_64bit.tar.gz`;
87
+ if (host.arch === 'arm64') return `arduino-cli_${ARDUINO_CLI_VERSION}_Linux_ARM64.tar.gz`;
88
+ if (host.arch === 'arm') return `arduino-cli_${ARDUINO_CLI_VERSION}_Linux_ARMv7.tar.gz`;
89
+ }
90
+
91
+ throw new Error(`Unsupported arduino-cli host platform ${host.os}-${host.arch}`);
92
+ };
93
+
94
+ const getArduinoCliInstallDir = (host = detectHostPlatform()) => {
95
+ return path.join(getArduinoCliRootDir(), `${host.os}-${host.arch}`);
96
+ };
97
+
98
+ export const getManagedArduinoCliPath = (host = detectHostPlatform()) => {
99
+ const installDir = getArduinoCliInstallDir(host);
100
+ return path.join(installDir, host.os === 'windows' ? 'arduino-cli.exe' : 'arduino-cli');
101
+ };
102
+
103
+ const getSupportedTargetsForHost = (host = detectHostPlatform()) => {
104
+ const targets = ['linux-arm', 'linux-arm64', 'linux-x64', 'windows-arm64', 'windows-x64'];
105
+ if (host.os === 'macos') {
106
+ targets.push('macos-arm64', 'macos-x64');
107
+ }
108
+ return targets;
109
+ };
110
+
111
+ const getZigArchiveName = (host = detectHostPlatform()) => {
112
+ if (host.os === 'windows') {
113
+ const archSegment = host.arch === 'arm64' ? 'aarch64' : host.arch === 'x64' ? 'x86_64' : null;
114
+ if (!archSegment) {
115
+ throw new Error(`Unsupported Windows host architecture for Zig: ${host.arch}`);
116
+ }
117
+ return `zig-${archSegment}-windows-${ZIG_VERSION}.zip`;
118
+ }
119
+
120
+ if (host.os === 'macos') {
121
+ const archSegment = host.arch === 'arm64' ? 'aarch64' : host.arch === 'x64' ? 'x86_64' : null;
122
+ if (!archSegment) {
123
+ throw new Error(`Unsupported macOS host architecture for Zig: ${host.arch}`);
124
+ }
125
+ return `zig-${archSegment}-macos-${ZIG_VERSION}.tar.xz`;
126
+ }
127
+
128
+ if (host.os === 'linux') {
129
+ if (host.arch === 'x64') return `zig-x86_64-linux-${ZIG_VERSION}.tar.xz`;
130
+ if (host.arch === 'arm64') return `zig-aarch64-linux-${ZIG_VERSION}.tar.xz`;
131
+ if (host.arch === 'arm') return `zig-arm-linux-${ZIG_VERSION}.tar.xz`;
132
+ }
133
+
134
+ throw new Error(`Unsupported Zig host platform ${host.os}-${host.arch}`);
135
+ };
136
+
137
+ const writeWrapper = (wrapperPath, zigBinaryPath, targetTriple) => {
138
+ fs.mkdirSync(path.dirname(wrapperPath), { recursive: true });
139
+
140
+ if (process.platform === 'win32') {
141
+ const windowsScript = [
142
+ '@echo off',
143
+ `"%~dp0..\\${path.basename(path.dirname(zigBinaryPath))}\\zig.exe" c++ -target ${targetTriple} %*`
144
+ ].join('\r\n');
145
+ fs.writeFileSync(wrapperPath, windowsScript);
146
+ return;
147
+ }
148
+
149
+ const posixScript = [
150
+ '#!/bin/sh',
151
+ `exec "${zigBinaryPath}" c++ -target ${targetTriple} "$@"`
152
+ ].join('\n');
153
+ fs.writeFileSync(wrapperPath, posixScript, { mode: 0o755 });
154
+ fs.chmodSync(wrapperPath, 0o755);
155
+ };
156
+
157
+ const ensureWrappers = (host = detectHostPlatform()) => {
158
+ const zigBinaryPath = getZigBinaryPath(host);
159
+ for (const target of getSupportedTargetsForHost(host)) {
160
+ const targetTriple = TARGET_TRIPLES[target];
161
+ if (!targetTriple) continue;
162
+ writeWrapper(getWrapperPath(target), zigBinaryPath, targetTriple);
163
+ }
164
+ };
165
+
166
+ export const detectHostPlatform = () => ({
167
+ os: normalizeHostOS(os.platform()),
168
+ arch: normalizeHostArch(os.arch())
169
+ });
170
+
171
+ export const getToolchainRoot = () => TOOLCHAIN_ROOT;
172
+
173
+ export const getManagedArduinoCliExecOptions = (overrides = {}) => ({
174
+ ...overrides,
175
+ env: {
176
+ ...process.env,
177
+ ARDUINO_DIRECTORIES_DATA: getArduinoCliDataDir(),
178
+ ARDUINO_DIRECTORIES_DOWNLOADS: getArduinoCliDownloadsDir(),
179
+ ARDUINO_DIRECTORIES_USER: getArduinoCliUserDir(),
180
+ ...(overrides.env || {})
181
+ }
182
+ });
183
+
184
+ export const isManagedZigCompiler = (compilerPath) => String(compilerPath || '').includes(`${path.sep}.nodalis${path.sep}toolchains${path.sep}zig${path.sep}`);
185
+
186
+ export const getDefaultToolchain = (host = detectHostPlatform()) => {
187
+ return getSupportedTargetsForHost(host).reduce((acc, target) => {
188
+ acc[target] = getWrapperPath(target);
189
+ return acc;
190
+ }, {});
191
+ };
192
+
193
+ const downloadFile = async (url, destinationPath) => {
194
+ await new Promise((resolve, reject) => {
195
+ const request = https.get(url, (response) => {
196
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
197
+ response.resume();
198
+ downloadFile(response.headers.location, destinationPath).then(resolve).catch(reject);
199
+ return;
200
+ }
201
+
202
+ if (response.statusCode !== 200) {
203
+ response.resume();
204
+ reject(new Error(`Download failed with HTTP ${response.statusCode} for ${url}`));
205
+ return;
206
+ }
207
+
208
+ const fileStream = fs.createWriteStream(destinationPath);
209
+ pipeline(response, fileStream).then(resolve).catch(reject);
210
+ });
211
+
212
+ request.on('error', reject);
213
+ });
214
+ };
215
+
216
+ const extractArchive = (archivePath, destinationDir) => {
217
+ fs.mkdirSync(destinationDir, { recursive: true });
218
+
219
+ if (archivePath.endsWith('.zip')) {
220
+ if (process.platform !== 'win32') {
221
+ throw new Error(`ZIP extraction is only configured for Windows hosts: ${archivePath}`);
222
+ }
223
+
224
+ execFileSync(
225
+ 'powershell.exe',
226
+ [
227
+ '-NoProfile',
228
+ '-NonInteractive',
229
+ '-Command',
230
+ `Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${destinationDir.replace(/'/g, "''")}' -Force`
231
+ ],
232
+ { stdio: 'inherit' }
233
+ );
234
+ return;
235
+ }
236
+
237
+ execFileSync('tar', ['-xf', archivePath, '-C', destinationDir], { stdio: 'inherit' });
238
+ };
239
+
240
+ const ensureArduinoCliDirs = () => {
241
+ fs.mkdirSync(getArduinoCliRootDir(), { recursive: true });
242
+ fs.mkdirSync(getArduinoCliDataDir(), { recursive: true });
243
+ fs.mkdirSync(getArduinoCliDownloadsDir(), { recursive: true });
244
+ fs.mkdirSync(getArduinoCliUserDir(), { recursive: true });
245
+ };
246
+
247
+ const installDefaultArduinoCores = (arduinoCliPath) => {
248
+ execFileSync(arduinoCliPath, ['core', 'update-index'], getManagedArduinoCliExecOptions({ stdio: 'inherit' }));
249
+
250
+ const coreIds = [...new Set(
251
+ DEFAULT_ARDUINO_FQBNS.map((fqbn) => {
252
+ const parts = String(fqbn).split(':');
253
+ return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : null;
254
+ }).filter(Boolean)
255
+ )];
256
+
257
+ for (const coreId of coreIds) {
258
+ execFileSync(arduinoCliPath, ['core', 'install', coreId], getManagedArduinoCliExecOptions({ stdio: 'inherit' }));
259
+ }
260
+ };
261
+
262
+ const installDefaultArduinoLibraries = (arduinoCliPath) => {
263
+ for (const libraryName of DEFAULT_ARDUINO_LIBRARIES) {
264
+ execFileSync(
265
+ arduinoCliPath,
266
+ ['lib', 'install', libraryName],
267
+ getManagedArduinoCliExecOptions({ stdio: 'inherit' })
268
+ );
269
+ }
270
+ };
271
+
272
+ export const installDefaultToolchains = async () => {
273
+ const host = detectHostPlatform();
274
+ const archiveName = getZigArchiveName(host);
275
+ const rootDir = path.join(TOOLCHAIN_ROOT, 'zig', ZIG_VERSION);
276
+ const archivePath = path.join(rootDir, archiveName);
277
+ const installDir = getZigInstallDir(host);
278
+ const zigBinaryPath = getZigBinaryPath(host);
279
+
280
+ fs.mkdirSync(rootDir, { recursive: true });
281
+
282
+ const results = [];
283
+ if (!fs.existsSync(zigBinaryPath)) {
284
+ const downloadUrl = `https://ziglang.org/download/${ZIG_VERSION}/${archiveName}`;
285
+
286
+ try {
287
+ await downloadFile(downloadUrl, archivePath);
288
+ extractArchive(archivePath, rootDir);
289
+ fs.rmSync(archivePath, { force: true });
290
+ results.push({
291
+ id: 'zig',
292
+ status: 'installed',
293
+ installDir,
294
+ downloadUrl
295
+ });
296
+ } catch (err) {
297
+ fs.rmSync(archivePath, { force: true });
298
+ results.push({
299
+ id: 'zig',
300
+ status: 'failed',
301
+ downloadUrl,
302
+ error: err.message
303
+ });
304
+ return {
305
+ host,
306
+ rootDir: TOOLCHAIN_ROOT,
307
+ toolchains: results,
308
+ defaultToolchain: getDefaultToolchain(host)
309
+ };
310
+ }
311
+ } else {
312
+ results.push({
313
+ id: 'zig',
314
+ status: 'already-installed',
315
+ installDir
316
+ });
317
+ }
318
+
319
+ ensureWrappers(host);
320
+
321
+ const arduinoArchiveName = getArduinoCliArchiveName(host);
322
+ const arduinoRootDir = getArduinoCliRootDir();
323
+ const arduinoArchivePath = path.join(arduinoRootDir, arduinoArchiveName);
324
+ const arduinoInstallDir = getArduinoCliInstallDir(host);
325
+ const arduinoCliPath = getManagedArduinoCliPath(host);
326
+
327
+ ensureArduinoCliDirs();
328
+
329
+ if (!fs.existsSync(arduinoCliPath)) {
330
+ const downloadUrl = `https://downloads.arduino.cc/arduino-cli/${arduinoArchiveName}`;
331
+
332
+ try {
333
+ await downloadFile(downloadUrl, arduinoArchivePath);
334
+ fs.mkdirSync(arduinoInstallDir, { recursive: true });
335
+ extractArchive(arduinoArchivePath, arduinoInstallDir);
336
+ fs.rmSync(arduinoArchivePath, { force: true });
337
+ results.push({
338
+ id: 'arduino-cli',
339
+ status: 'installed',
340
+ installDir: arduinoInstallDir,
341
+ downloadUrl
342
+ });
343
+ } catch (err) {
344
+ fs.rmSync(arduinoArchivePath, { force: true });
345
+ results.push({
346
+ id: 'arduino-cli',
347
+ status: 'failed',
348
+ downloadUrl,
349
+ error: err.message
350
+ });
351
+ return {
352
+ host,
353
+ rootDir: TOOLCHAIN_ROOT,
354
+ toolchains: results,
355
+ defaultToolchain: getDefaultToolchain(host),
356
+ arduinoCli: arduinoCliPath,
357
+ defaultArduinoFqbns: DEFAULT_ARDUINO_FQBNS
358
+ };
359
+ }
360
+ } else {
361
+ results.push({
362
+ id: 'arduino-cli',
363
+ status: 'already-installed',
364
+ installDir: arduinoInstallDir
365
+ });
366
+ }
367
+
368
+ try {
369
+ installDefaultArduinoCores(arduinoCliPath);
370
+ results.push({
371
+ id: 'arduino-default-cores',
372
+ status: 'installed',
373
+ fqbns: DEFAULT_ARDUINO_FQBNS
374
+ });
375
+ } catch (err) {
376
+ results.push({
377
+ id: 'arduino-default-cores',
378
+ status: 'failed',
379
+ fqbns: DEFAULT_ARDUINO_FQBNS,
380
+ error: err.message
381
+ });
382
+ }
383
+
384
+ try {
385
+ installDefaultArduinoLibraries(arduinoCliPath);
386
+ results.push({
387
+ id: 'arduino-default-libraries',
388
+ status: 'installed',
389
+ libraries: DEFAULT_ARDUINO_LIBRARIES
390
+ });
391
+ } catch (err) {
392
+ results.push({
393
+ id: 'arduino-default-libraries',
394
+ status: 'failed',
395
+ libraries: DEFAULT_ARDUINO_LIBRARIES,
396
+ error: err.message
397
+ });
398
+ }
399
+
400
+ return {
401
+ host,
402
+ rootDir: TOOLCHAIN_ROOT,
403
+ toolchains: [
404
+ ...results,
405
+ ...getSupportedTargetsForHost(host).map((target) => ({
406
+ id: target,
407
+ status: 'ready',
408
+ compiler: getWrapperPath(target),
409
+ targetTriple: TARGET_TRIPLES[target]
410
+ }))
411
+ ],
412
+ defaultToolchain: getDefaultToolchain(host),
413
+ arduinoCli: arduinoCliPath,
414
+ defaultArduinoFqbns: DEFAULT_ARDUINO_FQBNS,
415
+ defaultArduinoLibraries: DEFAULT_ARDUINO_LIBRARIES
416
+ };
417
+ };