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 +6 -1
- package/README.md +34 -10
- package/package.json +1 -1
- package/src/compilers/ArduinoCompiler.js +20 -13
- package/src/compilers/CPPCompiler.js +34 -39
- package/src/compilers/JSCompiler.js +4 -2
- package/src/compilers/arduinoDefaults.js +5 -0
- package/src/compilers/st-parser/parser.js +120 -3
- package/src/compilers/support/arduino/nodalis.h +3 -10
- package/src/nodalis.js +24 -2
- package/src/programmers/ArduinoProgrammer.js +7 -1
- package/src/programmers/FileProgrammer.js +1 -0
- package/src/programmers/MTIProgrammer.js +1 -0
- package/src/programmers/SSHProgrammer.js +1 -0
- package/src/toolchains.js +417 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [1.0.
|
|
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
|
|
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": "
|
|
122
|
-
"linux-arm64": "
|
|
123
|
-
"linux-x64": "
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
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
|
-
-
|
|
132
|
-
-
|
|
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
|
@@ -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
|
|
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 ||
|
|
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}".
|
|
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(
|
|
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 = { ...
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
`
|
|
540
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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());
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
|
@@ -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
|
+
};
|