nodalis-compiler 1.0.22 → 1.0.24

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,15 @@
1
1
  # Changelog
2
2
 
3
- ## [1.0.22] 2026-03-03
3
+ ## [1.0.24] 2026-03-04
4
+ - Added support for CONFIGURATION, RESOURCE, TASK, and PROGRAM (instance) keywords in ST.
5
+ - Added "required" arrays to programmers for communicating required parameters beyond the base params when calling program.
6
+
7
+ ## [1.0.23] 2026-03-03
4
8
  - Changed IEC parser to interpret Function Blocks that are actually standard functions to a formal function call.
5
9
  - Fixed nodejs/jint compiles to put executables in a bin folder.
6
10
  - Fixed syntax errors with repeat.
7
11
  - Prevent stale files in ST bundle compile.
12
+ - Added a new "action" for "get-toolchains", which will download the necessary toolchains for CPP and Arduino.
8
13
 
9
14
  ## [1.0.17] 2026-02-25
10
15
  - 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.24",
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",
@@ -21,16 +21,14 @@ import { fileURLToPath } from 'node:url';
21
21
  import { dirname } from 'node:path';
22
22
  import { Compiler, IECLanguage, OutputType, CommunicationProtocol } from './Compiler.js';
23
23
  import * as iec from './iec-parser/parser.js';
24
- import { parseStructuredText } from './st-parser/parser.js';
24
+ import { parseStructuredText, buildCompilerMetadataDirectives } 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',
@@ -163,7 +161,9 @@ export class ArduinoCompiler extends Compiler {
163
161
  let taskCode = '';
164
162
  let mapCode = '';
165
163
 
166
- const lines = sourceCode.split('\n');
164
+ const metadataDirectives = buildCompilerMetadataDirectives(parsed);
165
+ const metadataAwareSource = metadataDirectives.length > 0 ? `${metadataDirectives}${sourceCode}` : sourceCode;
166
+ const lines = metadataAwareSource.split('\n');
167
167
  lines.forEach((line) => {
168
168
  if (line.trim().startsWith('//Task=')) {
169
169
  const task = JSON.parse(line.substring(line.indexOf('=') + 1).trim());
@@ -257,7 +257,7 @@ void loop() {
257
257
  fs.cpSync(path.join(supportDir, 'json.hpp'), path.join(outputPath, 'json.hpp'), { force: true });
258
258
 
259
259
  if (this.isExecutableOutput()) {
260
- const arduinoCli = compilerConfig.arduino_cli || compilerConfig.arduinoCli || 'arduino-cli';
260
+ const arduinoCli = compilerConfig.arduino_cli || compilerConfig.arduinoCli || getManagedArduinoCliPath();
261
261
  const arduinoFqbn = this.resolveArduinoFqbn(target, compilerConfig);
262
262
  if (!arduinoFqbn) {
263
263
  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 +272,18 @@ void loop() {
272
272
  fs.mkdirSync(binDir, { recursive: true });
273
273
  const arduinoCompileCmd = `${arduinoCli} compile --fqbn "${arduinoFqbn}" "${outputPath}" --build-path "${buildDir}" --output-dir "${binDir}" --export-binaries`;
274
274
  try {
275
- execSync(arduinoCompileCmd, { stdio: 'pipe' });
275
+ execSync(arduinoCompileCmd, getManagedArduinoCliExecOptions({ stdio: 'pipe' }));
276
276
  } catch (err) {
277
277
  const stderrText = getExecOutputText(err?.stderr);
278
278
  const stdoutText = getExecOutputText(err?.stdout);
279
279
  const compilerOutput = [stderrText, stdoutText].filter(Boolean).join('\n');
280
+ const missingArduinoLibrary = compilerOutput.includes('ArduinoModbus.h') || compilerOutput.includes('No such file or directory');
280
281
  const details = compilerOutput || err.message;
281
282
  throw new Error(
282
- `Arduino CLI build failed for "${arduinoFqbn}". Verify arduino-cli and board core availability.\n${details}`
283
+ `Arduino CLI build failed for "${arduinoFqbn}". ` +
284
+ `${missingArduinoLibrary
285
+ ? 'Run "nodalis --action get-toolchains" to install the managed Arduino cores and libraries.'
286
+ : 'Verify arduino-cli, board core, and library availability.'}\n${details}`
283
287
  );
284
288
  }
285
289
  }
@@ -353,7 +357,7 @@ void loop() {
353
357
 
354
358
  let coreList = '';
355
359
  try {
356
- coreList = execSync(`${arduinoCli} core list`, { encoding: 'utf8' });
360
+ coreList = execSync(`${arduinoCli} core list`, getManagedArduinoCliExecOptions({ encoding: 'utf8' }));
357
361
  } catch (err) {
358
362
  throw new Error(`Failed to query installed Arduino cores using "${arduinoCli}". ${err.message}`);
359
363
  }
@@ -362,8 +366,8 @@ void loop() {
362
366
  }
363
367
 
364
368
  try {
365
- execSync(`${arduinoCli} core update-index`, { stdio: 'inherit' });
366
- execSync(`${arduinoCli} core install ${coreId}`, { stdio: 'inherit' });
369
+ execSync(`${arduinoCli} core update-index`, getManagedArduinoCliExecOptions({ stdio: 'inherit' }));
370
+ execSync(`${arduinoCli} core install ${coreId}`, getManagedArduinoCliExecOptions({ stdio: 'inherit' }));
367
371
  } catch (err) {
368
372
  throw new Error(`Failed to install Arduino core "${coreId}" required for ${arduinoFqbn}. ${err.message}`);
369
373
  }
@@ -387,7 +391,10 @@ void loop() {
387
391
  const targets = Object.keys(DEFAULT_ARDUINO_FQBN);
388
392
 
389
393
  try {
390
- const boardListRaw = execSync('arduino-cli board listall --format json', { encoding: 'utf8' });
394
+ const boardListRaw = execSync(
395
+ `${getManagedArduinoCliPath()} board listall --format json`,
396
+ getManagedArduinoCliExecOptions({ encoding: 'utf8' })
397
+ );
391
398
  const boardList = JSON.parse(boardListRaw);
392
399
  const boards = Array.isArray(boardList.boards) ? boardList.boards : [];
393
400
 
@@ -20,23 +20,16 @@ import fs from 'fs';
20
20
  import path from "path";
21
21
  import { Compiler, IECLanguage, OutputType, CommunicationProtocol } from './Compiler.js';
22
22
  import * as iec from "./iec-parser/parser.js";
23
- import { parseStructuredText } from './st-parser/parser.js';
23
+ import { parseStructuredText, buildCompilerMetadataDirectives } 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)) {
@@ -153,7 +146,9 @@ export class CPPCompiler extends Compiler {
153
146
  if(typeof resourceName !== "undefined" && resourceName !== null){
154
147
  plcname = resourceName;
155
148
  }
156
- const lines = sourceCode.split("\n");
149
+ const metadataDirectives = buildCompilerMetadataDirectives(parsed);
150
+ const metadataAwareSource = metadataDirectives.length > 0 ? `${metadataDirectives}${sourceCode}` : sourceCode;
151
+ const lines = metadataAwareSource.split("\n");
157
152
  lines.forEach((line) => {
158
153
  if(line.trim().startsWith("//Task=")){
159
154
  var task = JSON.parse(line.substring(line.indexOf("=") + 1).trim());
@@ -369,7 +364,12 @@ int main() {
369
364
  .forEach((dir) => searchDirs.add(dir));
370
365
  }
371
366
 
372
- const compilerPath = execSync(`which "${compiler}"`, { encoding: 'utf8' }).trim();
367
+ const compilerPath = path.isAbsolute(compiler)
368
+ ? compiler
369
+ : execSync(process.platform === 'win32' ? `where "${compiler}"` : `which "${compiler}"`, { encoding: 'utf8' })
370
+ .split(/\r?\n/)
371
+ .map((line) => line.trim())
372
+ .find(Boolean);
373
373
  if (compilerPath) {
374
374
  const compilerDir = path.dirname(compilerPath);
375
375
  searchDirs.add(compilerDir);
@@ -500,44 +500,28 @@ int main() {
500
500
  }
501
501
 
502
502
  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
503
  const targetKey = `${targetOs}-${targetArch}`;
510
504
 
511
505
  const ensureCompilerAvailable = (compilerName, message) => {
512
- const versionCommand = compilerName === "cl.exe" ? compilerName : `${compilerName} --version`;
513
506
  try {
514
- execSync(versionCommand, { stdio: "ignore" });
507
+ execSync(`"${compilerName}" --version`, { stdio: 'ignore' });
515
508
  } catch {
516
509
  throw new Error(message);
517
510
  }
518
511
  };
519
512
 
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
513
  const configuredCompiler = ToolChain[targetKey];
534
514
  if (!configuredCompiler) {
535
- throw new Error(`No cross-compiler is configured for target ${targetKey}. Add it to toolchain.json or update the ToolChain defaults.`);
515
+ throw new Error(
516
+ `No toolchain is configured for target ${targetKey} on host ${hostOs}-${hostArch}. ` +
517
+ `Run "nodalis --action get-toolchains" or add an override in toolchain.json.`
518
+ );
536
519
  }
537
520
  ensureCompilerAvailable(
538
521
  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.`
522
+ `Toolchain "${configuredCompiler}" for target ${targetKey} is not available. ` +
523
+ `Run "nodalis --action get-toolchains" to install managed toolchains under ${getToolchainRoot()}, ` +
524
+ `or add a custom compiler path in toolchain.json.`
541
525
  );
542
526
  return configuredCompiler;
543
527
  }
@@ -561,7 +545,7 @@ int main() {
561
545
  if (compiler === 'cl.exe') {
562
546
  return { c: [], cpp: [] };
563
547
  }
564
- let flags = { //default flags are for macos clang
548
+ let flags = { // default flags are for managed/native clang wrappers
565
549
  'linux-x64': [],
566
550
  'linux-arm64': [],
567
551
  'linux-arm': [],
@@ -581,7 +565,18 @@ int main() {
581
565
  "macos-arm64": "",
582
566
  }
583
567
 
584
- if (!compiler.includes("clang")) {
568
+ if (isManagedZigCompiler(compiler)) {
569
+ flags = {
570
+ 'linux-x64': [],
571
+ 'linux-arm64': [],
572
+ 'linux-arm': [],
573
+ 'macos-x64': [],
574
+ 'macos-arm64': [],
575
+ 'windows-x64': [],
576
+ 'windows-arm64': []
577
+ };
578
+ }
579
+ else if (!compiler.includes("clang")) {
585
580
  flags = {
586
581
  'linux-x64': ["-D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE -pthread"],
587
582
  'linux-arm64': ["-D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE -pthread"],
@@ -19,7 +19,7 @@ import os from "os";
19
19
  import path from "path";
20
20
  import { Compiler, IECLanguage, OutputType, CommunicationProtocol } from './Compiler.js';
21
21
  import * as iec from "./iec-parser/parser.js";
22
- import { parseStructuredText } from './st-parser/parser.js';
22
+ import { parseStructuredText, buildCompilerMetadataDirectives } from './st-parser/parser.js';
23
23
  import { transpile } from './st-parser/jstranspiler.js';
24
24
  import which from "which";
25
25
  import { fileURLToPath } from "url";
@@ -114,7 +114,9 @@ export class JSCompiler extends Compiler {
114
114
  if(typeof resourceName !== "undefined" && resourceName !== null){
115
115
  plcname = resourceName;
116
116
  }
117
- const lines = sourceCode.split("\n");
117
+ const metadataDirectives = buildCompilerMetadataDirectives(parsed);
118
+ const metadataAwareSource = metadataDirectives.length > 0 ? `${metadataDirectives}${sourceCode}` : sourceCode;
119
+ const lines = metadataAwareSource.split("\n");
118
120
  lines.forEach((line) => {
119
121
  if(line.trim().startsWith("//Task=")){
120
122
  var task = JSON.parse(line.substring(line.indexOf("=") + 1).trim());
@@ -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);
@@ -32,6 +32,15 @@ import { tokenize } from './tokenizer.js';
32
32
  export function parseStructuredText(code) {
33
33
  const tokens = tokenize(code);
34
34
  let position = 0;
35
+ const TOP_LEVEL_STARTERS = new Set([
36
+ 'PROGRAM',
37
+ 'FUNCTION',
38
+ 'FUNCTION_BLOCK',
39
+ 'VAR_GLOBAL',
40
+ 'CONFIGURATION',
41
+ 'RESOURCE',
42
+ 'TASK'
43
+ ]);
35
44
 
36
45
  function peek(offset = 0) {
37
46
  return tokens[position + offset];
@@ -55,19 +64,101 @@ export function parseStructuredText(code) {
55
64
 
56
65
  switch (token.value.toUpperCase()) {
57
66
  case 'PROGRAM':
58
- return parseProgram();
67
+ return parseProgramOrInstance();
59
68
  case 'FUNCTION':
60
69
  return parseFunction();
61
70
  case 'FUNCTION_BLOCK':
62
71
  return parseFunctionBlock();
63
72
  case 'VAR_GLOBAL':
64
73
  return parseGlobalVarSection();
74
+ case 'TASK':
75
+ return parseTask();
76
+ case 'CONFIGURATION':
77
+ return parseContainer('CONFIGURATION', 'END_CONFIGURATION');
78
+ case 'RESOURCE':
79
+ return parseContainer('RESOURCE', 'END_RESOURCE');
65
80
  default:
66
81
  consume();
67
82
  return null;
68
83
  }
69
84
  }
70
85
 
86
+ function parseContainer(startKeyword, endKeyword) {
87
+ expect(startKeyword);
88
+ const blocks = [];
89
+
90
+ while (peek() && peek().value.toUpperCase() !== endKeyword) {
91
+ const upper = peek().value.toUpperCase();
92
+ if (TOP_LEVEL_STARTERS.has(upper)) {
93
+ const block = parseBlock();
94
+ if (Array.isArray(block)) blocks.push(...block);
95
+ else if (block) blocks.push(block);
96
+ } else {
97
+ consume();
98
+ }
99
+ }
100
+
101
+ expect(endKeyword);
102
+ return blocks;
103
+ }
104
+
105
+ function parseTask() {
106
+ expect('TASK');
107
+ const name = consume().value;
108
+ let interval = '1000';
109
+ let priority = '1';
110
+
111
+ if (peek()?.value === '(') {
112
+ consume();
113
+ while (peek() && peek().value !== ')') {
114
+ const key = consume().value.toUpperCase();
115
+ if (peek()?.value === ':=') consume();
116
+
117
+ const valueTokens = [];
118
+ while (peek() && peek().value !== ',' && peek().value !== ')') {
119
+ valueTokens.push(consume().value);
120
+ }
121
+ const value = valueTokens.join('');
122
+
123
+ if (key === 'INTERVAL') interval = normalizeTaskInterval(value);
124
+ else if (key === 'PRIORITY') priority = normalizeNumericString(value, '1');
125
+
126
+ if (peek()?.value === ',') consume();
127
+ }
128
+ expect(')');
129
+ }
130
+
131
+ if (peek()?.value === ';') consume();
132
+ return { type: 'TaskDeclaration', name, interval, priority };
133
+ }
134
+
135
+ function normalizeNumericString(value, fallback) {
136
+ const match = String(value || '').match(/-?\d+/);
137
+ return match ? match[0] : fallback;
138
+ }
139
+
140
+ function normalizeTaskInterval(value) {
141
+ const text = String(value || '').trim();
142
+ if (!text) return '1000';
143
+
144
+ if (/^-?\d+$/.test(text)) return text;
145
+
146
+ const duration = text.match(/^T#?([+-]?\d+(?:\.\d+)?)(MS|S|M|H|D)?$/i);
147
+ if (!duration) return normalizeNumericString(text, '1000');
148
+
149
+ const amount = Number(duration[1]);
150
+ const unit = (duration[2] || 'MS').toUpperCase();
151
+ const multipliers = {
152
+ MS: 1,
153
+ S: 1000,
154
+ M: 60000,
155
+ H: 3600000,
156
+ D: 86400000
157
+ };
158
+ const ms = Math.round(amount * (multipliers[unit] || 1));
159
+ return String(ms);
160
+ }
161
+
71
162
  function parseGlobalVarSection() {
72
163
  expect('VAR_GLOBAL');
73
164
  const variables = [];
@@ -342,9 +433,22 @@ function parseStatementsUntil(endTokens) {
342
433
  return peek(1)?.value === ':' || peek(1)?.value === ',';
343
434
  }
344
435
 
345
- function parseProgram() {
436
+ function parseProgramOrInstance() {
346
437
  expect('PROGRAM');
347
438
  const name = consume().value;
439
+
440
+ if (peek()?.value?.toUpperCase() === 'WITH' || peek()?.value === ':') {
441
+ let associatedTaskName = '';
442
+ if (peek()?.value?.toUpperCase() === 'WITH') {
443
+ consume();
444
+ associatedTaskName = consume().value;
445
+ }
446
+ expect(':');
447
+ const typeName = consume().value;
448
+ if (peek()?.value === ';') consume();
449
+ return { type: 'ProgramInstanceDeclaration', name, typeName, associatedTaskName };
450
+ }
451
+
348
452
  const vars = [];
349
453
  const stmts = [];
350
454
 
@@ -395,8 +499,21 @@ function parseStatementsUntil(endTokens) {
395
499
  const body = [];
396
500
  while (position < tokens.length) {
397
501
  const block = parseBlock();
398
- if (block) body.push(block);
502
+ if (Array.isArray(block)) body.push(...block);
503
+ else if (block) body.push(block);
399
504
  }
400
505
 
401
506
  return { type: 'Program', body };
402
507
  }
508
+
509
+ export function buildCompilerMetadataDirectives(ast) {
510
+ const lines = [];
511
+ for (const block of ast?.body || []) {
512
+ if (block?.type === 'TaskDeclaration') {
513
+ lines.push(`//Task={"Name":"${block.name}", "Interval":"${block.interval}", "Priority":"${block.priority}"}`);
514
+ } else if (block?.type === 'ProgramInstanceDeclaration') {
515
+ lines.push(`//Instance={"TypeName":"${block.typeName}", "Name":"${block.name}", "AssociatedTaskName":"${block.associatedTaskName || ''}"}`);
516
+ }
517
+ }
518
+ return lines.join('\n') + (lines.length > 0 ? '\n' : '');
519
+ }
@@ -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);
@@ -103,7 +104,8 @@ export class Nodalis {
103
104
  listProgrammers() {
104
105
  return this.programmers.map(p => ({
105
106
  name: p.name,
106
- target: p.target
107
+ target: p.target,
108
+ required: p.required
107
109
  }));
108
110
  }
109
111
 
@@ -172,6 +174,10 @@ export class Nodalis {
172
174
  }
173
175
  }
174
176
 
177
+ async getToolchains() {
178
+ return installDefaultToolchains();
179
+ }
180
+
175
181
  }
176
182
 
177
183
  function isCliEntryPoint() {
@@ -221,6 +227,11 @@ Actions:
221
227
  --sshPort Optional SSH port for SSH deployment.
222
228
  --arduinoFqbn Required for Arduino target if --target Arduino.
223
229
 
230
+ --action get-toolchains
231
+ Detects the host OS/arch and installs managed C/C++ toolchains,
232
+ arduino-cli, and default Arduino board cores under:
233
+ ${getToolchainRoot()}
234
+
224
235
  Examples:
225
236
  node nodalis.js --action list-compilers
226
237
 
@@ -231,6 +242,8 @@ Examples:
231
242
  --resourceName MyPLC \\
232
243
  --sourcePath ./examples/pump.iec \\
233
244
  --language st
245
+
246
+ node nodalis.js --action get-toolchains
234
247
  `);
235
248
  process.exit(0);
236
249
  }
@@ -296,9 +309,18 @@ Examples:
296
309
  break;
297
310
  }
298
311
 
312
+ case 'get-toolchains': {
313
+ app.getToolchains().then((result) => {
314
+ console.log(JSON.stringify(result, null, 2));
315
+ }).catch(err => {
316
+ console.error(`Toolchain installation failed: ${err.message}`);
317
+ });
318
+ break;
319
+ }
320
+
299
321
  default: {
300
322
  console.error(`Unknown or missing action: ${argMap.action}`);
301
- console.error(`Valid actions: list-compilers, list-programmers, compile, deploy`);
323
+ console.error(`Valid actions: list-compilers, list-programmers, compile, deploy, get-toolchains`);
302
324
  break;
303
325
  }
304
326
  }
@@ -16,12 +16,14 @@ 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) {
22
23
  super(options);
23
24
  this.name = 'ArduinoProgrammer';
24
25
  this.target = 'arduino';
26
+ this.required = ["fqbn"];
25
27
  }
26
28
 
27
29
  async program() {
@@ -58,7 +60,11 @@ export class ArduinoProgrammer extends Programmer {
58
60
  }
59
61
  }
60
62
 
61
- const { stdout, stderr } = await runCommand('arduino-cli', args);
63
+ const { stdout, stderr } = await runCommand(
64
+ getManagedArduinoCliPath(),
65
+ args,
66
+ getManagedArduinoCliExecOptions()
67
+ );
62
68
  if (stdout) {
63
69
  console.log(stdout.trim());
64
70
  }
@@ -32,6 +32,7 @@ export class FileProgrammer extends Programmer {
32
32
  super(options);
33
33
  this.name = 'FileProgrammer';
34
34
  this.target = 'FILE';
35
+ this.required = [];
35
36
  }
36
37
 
37
38
  async program() {
@@ -26,6 +26,7 @@ export class MTIProgrammer extends Programmer {
26
26
  super(options);
27
27
  this.name = "MTIProgrammer";
28
28
  this.target = "MTI";
29
+ this.required = [];
29
30
  }
30
31
 
31
32
  async program() {
@@ -37,6 +37,7 @@ export class SSHProgrammer extends Programmer {
37
37
  super(options);
38
38
  this.name = 'SSHProgrammer';
39
39
  this.target = 'SSH';
40
+ this.required = ["username", "password"];
40
41
  }
41
42
 
42
43
  async program() {
@@ -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
+ };