@tsplc/build 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +6 -2
- package/dist/cli.js +1652 -0
- package/dist/cli.js.map +34 -0
- package/dist/index.js +1552 -0
- package/dist/index.js.map +31 -0
- package/dist/libraries.js +128 -0
- package/dist/libraries.js.map +10 -0
- package/docker/Dockerfile +173 -0
- package/docker/Dockerfile.dockerignore +17 -0
- package/docker/Dockerfile.source +15 -0
- package/docker/build-image +38 -0
- package/docker/codesys-build +5 -0
- package/docker/config.ts +8 -0
- package/docker/control-linux-sl/Dockerfile +38 -0
- package/docker/entrypoint.sh +10 -0
- package/docker/install/NOTICE.md +21 -0
- package/docker/install/core.sh +36 -0
- package/docker/install/decode.c +12 -0
- package/docker/install/device/Runtime.ico +0 -0
- package/docker/install/device/Runtime.png +0 -0
- package/docker/install/device/codesyscontrol.devdesc.xml +1502 -0
- package/docker/install/extract.py +43 -0
- package/docker/install/helper-launcher.c +46 -0
- package/docker/install/install.sh +94 -0
- package/docker/install/prerequisites.sh +15 -0
- package/docker/install/verify.py +133 -0
- package/docker/install/verify.sh +34 -0
- package/docker/install/wine-ncrypt/COPYING.LIB +502 -0
- package/docker/install/wine-ncrypt/README.md +28 -0
- package/docker/install/wine-ncrypt/enable-oaep.py +34 -0
- package/docker/install/wine-ncrypt/main.c +619 -0
- package/docker/install/wine-ncrypt/oaep.patch +11 -0
- package/docker/install/wine-signal/README.md +22 -0
- package/docker/install/wine-signal/build.sh +10 -0
- package/docker/install/wine-signal/check.sh +34 -0
- package/docker/install/wine-signal/noop-i386.c +2 -0
- package/docker/install/wine-signal/signal-state.c +105 -0
- package/docker/install/wine-signal/tests/avx-registers.c +53 -0
- package/docker/install/wine-signal/tests/frame-and-handler.c +192 -0
- package/docker/install/wine-signal/tests/loader-i386.c +8 -0
- package/docker/install/wine-signal/tests/signal-return.c +47 -0
- package/docker/install/wine-signal/wine +4 -0
- package/docker/runner.ts +131 -0
- package/package.json +53 -4
- package/python/build.py +655 -0
- package/python/codesys_online.py +300 -0
- package/python/test_build.py +444 -0
- package/python/test_codesys_online.py +170 -0
- package/python/test_installer_packages.py +216 -0
- package/python/test_installer_verification.py +307 -0
- package/python/test_worker.py +142 -0
- package/python/worker.py +89 -0
- package/runtime/install.sh +74 -0
- package/src/__tests__/fixtures/xml/fb-simple.xml +40 -0
- package/src/__tests__/fixtures/xml/fb-state-machine.xml +86 -0
- package/src/__tests__/fixtures/xml/prg-cyclic.xml +44 -0
- package/src/artifacts.ts +77 -0
- package/src/build.ts +87 -0
- package/src/cli.ts +116 -0
- package/src/config.ts +12 -0
- package/src/controller-online.ts +143 -0
- package/src/controller-session.ts +229 -0
- package/src/diagnostics.ts +66 -0
- package/src/docker-build.ts +148 -0
- package/src/docker-context.ts +35 -0
- package/src/docker-run.ts +104 -0
- package/src/host/codesys-command.ts +45 -0
- package/src/host/codesys-host.ts +145 -0
- package/src/host/codesys-worker.ts +345 -0
- package/src/host/windows-path.ts +73 -0
- package/src/index.ts +26 -0
- package/src/io.ts +127 -0
- package/src/libraries.ts +166 -0
- package/src/package-path.ts +5 -0
- package/src/run.ts +133 -0
- package/src/runtime-status.ts +21 -0
- package/src/sources.ts +248 -0
- package/src/st/declarations.ts +229 -0
- package/src/st/lexer.ts +125 -0
- package/src/st/parse.ts +202 -0
- package/src/st/reader.ts +50 -0
package/src/st/parse.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
declarationKeywords,
|
|
4
|
+
globalSections,
|
|
5
|
+
objectKinds,
|
|
6
|
+
readDeclarationPragmas,
|
|
7
|
+
readDeclarationSections,
|
|
8
|
+
readDeclarationSemicolon,
|
|
9
|
+
readIdentifier,
|
|
10
|
+
readModifiers,
|
|
11
|
+
readType,
|
|
12
|
+
readVariableSection,
|
|
13
|
+
variableSections,
|
|
14
|
+
} from "./declarations";
|
|
15
|
+
import type { Token } from "./lexer";
|
|
16
|
+
import { createTokenReader, type TokenReader } from "./reader";
|
|
17
|
+
|
|
18
|
+
export type ObjectKind =
|
|
19
|
+
(typeof objectKinds)[keyof typeof objectKinds] | "dut" | "gvl" | "action" | "get" | "set";
|
|
20
|
+
export type ParsedSource = {
|
|
21
|
+
kind: ObjectKind;
|
|
22
|
+
name: string;
|
|
23
|
+
declaration: string;
|
|
24
|
+
implementation?: string;
|
|
25
|
+
returnType?: string;
|
|
26
|
+
dutType?: "structure" | "enumeration" | "union" | "alias";
|
|
27
|
+
persistent?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ParseOptions = {
|
|
31
|
+
filename?: string;
|
|
32
|
+
declarationOnly?: boolean;
|
|
33
|
+
name?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const dutTypes: Record<string, NonNullable<ParsedSource["dutType"]>> = {
|
|
37
|
+
STRUCT: "structure",
|
|
38
|
+
UNION: "union",
|
|
39
|
+
"(": "enumeration",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const parseDut = (reader: TokenReader): ParsedSource => {
|
|
43
|
+
reader.expect("TYPE");
|
|
44
|
+
readModifiers(reader);
|
|
45
|
+
const name = readIdentifier(reader);
|
|
46
|
+
if (reader.consume("EXTENDS")) {
|
|
47
|
+
readType(reader);
|
|
48
|
+
}
|
|
49
|
+
reader.expect(":");
|
|
50
|
+
readDeclarationPragmas(reader);
|
|
51
|
+
const dutType = dutTypes[reader.peek()?.value ?? ""] ?? "alias";
|
|
52
|
+
while (reader.peek()?.value !== "END_TYPE") {
|
|
53
|
+
if (reader.peek()?.value === "TYPE") {
|
|
54
|
+
reader.fail("Only one TYPE declaration is allowed per file");
|
|
55
|
+
}
|
|
56
|
+
reader.take();
|
|
57
|
+
}
|
|
58
|
+
reader.take();
|
|
59
|
+
reader.consume(";");
|
|
60
|
+
if (reader.peek()) {
|
|
61
|
+
reader.fail("Unexpected content after END_TYPE");
|
|
62
|
+
}
|
|
63
|
+
return { kind: "dut", name, declaration: reader.source, dutType };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const parseGlobals = (reader: TokenReader, options: ParseOptions): ParsedSource => {
|
|
67
|
+
const header = reader.peek();
|
|
68
|
+
let persistent = false;
|
|
69
|
+
while (reader.peek()) {
|
|
70
|
+
readDeclarationPragmas(reader);
|
|
71
|
+
if (!reader.peek()) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (!globalSections.has(reader.peek()?.value ?? "")) {
|
|
75
|
+
reader.fail("Expected a global variable section");
|
|
76
|
+
}
|
|
77
|
+
const start = reader.index;
|
|
78
|
+
readVariableSection(reader);
|
|
79
|
+
// Only the section modifiers select the CODESYS persistent-list object.
|
|
80
|
+
// Comments, pragma strings, and variable names cannot select this role.
|
|
81
|
+
for (const token of reader.tokens.slice(start + 1, reader.index)) {
|
|
82
|
+
if (
|
|
83
|
+
token.kind !== "word" ||
|
|
84
|
+
!["CONSTANT", "RETAIN", "PERSISTENT", "NON_RETAIN"].includes(token.value)
|
|
85
|
+
) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
persistent ||= token.value === "PERSISTENT";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const name = options.name ?? basename(reader.filename, ".st");
|
|
92
|
+
if (!/^[A-Za-z_][A-Za-z_0-9]*$/.test(name)) {
|
|
93
|
+
reader.fail("Global list requires an IEC filename or an explicit name", header);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
kind: "gvl",
|
|
97
|
+
name,
|
|
98
|
+
declaration: reader.source,
|
|
99
|
+
...(persistent && { persistent: true }),
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const validateImplementation = (reader: TokenReader, tokens: Token[]) => {
|
|
104
|
+
for (const token of tokens) {
|
|
105
|
+
if (token.kind !== "word") {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (variableSections.has(token.value) || token.value === "END_VAR") {
|
|
109
|
+
reader.fail(
|
|
110
|
+
"Variable declaration outside the declaration section; conditional declaration envelopes are not supported",
|
|
111
|
+
token,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (declarationKeywords.has(token.value)) {
|
|
115
|
+
reader.fail(
|
|
116
|
+
"Nested objects must be separate child files, or the object terminator is mismatched",
|
|
117
|
+
token,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const splitObject = (
|
|
124
|
+
reader: TokenReader,
|
|
125
|
+
header: Token,
|
|
126
|
+
kind: ObjectKind,
|
|
127
|
+
options: ParseOptions,
|
|
128
|
+
): Pick<ParsedSource, "declaration" | "implementation"> => {
|
|
129
|
+
const terminator =
|
|
130
|
+
header.value === "FUNCTIONBLOCK" ? "END_FUNCTION_BLOCK" : `END_${header.value}`;
|
|
131
|
+
const remaining = reader.tokens.slice(reader.index);
|
|
132
|
+
const terminatorIndex = remaining.findIndex(
|
|
133
|
+
(token) => token.kind === "word" && token.value === terminator,
|
|
134
|
+
);
|
|
135
|
+
// Even interfaces and properties need END_* in combined files. Only an explicit
|
|
136
|
+
// declaration-only input (e.g. a paired declaration.st) may omit it.
|
|
137
|
+
if (terminatorIndex < 0 && !options.declarationOnly) {
|
|
138
|
+
reader.fail(`Missing ${terminator}`);
|
|
139
|
+
}
|
|
140
|
+
const bodyTokens = terminatorIndex < 0 ? remaining : remaining.slice(0, terminatorIndex);
|
|
141
|
+
const [endToken, next, afterSemicolon] =
|
|
142
|
+
terminatorIndex < 0 ? [] : remaining.slice(terminatorIndex);
|
|
143
|
+
const semicolon = next?.value === ";" ? next : undefined;
|
|
144
|
+
const unexpected = semicolon ? afterSemicolon : next;
|
|
145
|
+
if (unexpected) {
|
|
146
|
+
reader.fail(`Unexpected content after ${terminator}`, unexpected);
|
|
147
|
+
}
|
|
148
|
+
validateImplementation(reader, bodyTokens);
|
|
149
|
+
|
|
150
|
+
const declarationOnly = options.declarationOnly || kind === "interface" || kind === "property";
|
|
151
|
+
if (declarationOnly && bodyTokens.length) {
|
|
152
|
+
reader.fail("Unexpected implementation in a declaration-only object", bodyTokens[0]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const declaration = reader.source.slice(0, reader.end);
|
|
156
|
+
// Remove only END_* and its optional semicolon. Keep whitespace and trailing
|
|
157
|
+
// comments in the body, or in the declaration for objects without a body.
|
|
158
|
+
const body =
|
|
159
|
+
reader.source.slice(reader.end, endToken?.start) +
|
|
160
|
+
(endToken ? reader.source.slice((semicolon ?? endToken).end) : "");
|
|
161
|
+
if (declarationOnly) {
|
|
162
|
+
return { declaration: declaration + body };
|
|
163
|
+
}
|
|
164
|
+
return { declaration, implementation: body };
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const parsePou = (reader: TokenReader, options: ParseOptions): ParsedSource => {
|
|
168
|
+
const header = reader.take();
|
|
169
|
+
if (!(header.value in objectKinds)) {
|
|
170
|
+
reader.fail(`Unsupported object header ${header.value}`, header);
|
|
171
|
+
}
|
|
172
|
+
const kind = objectKinds[header.value as keyof typeof objectKinds];
|
|
173
|
+
readModifiers(reader);
|
|
174
|
+
const name = readIdentifier(reader);
|
|
175
|
+
const returnType = reader.consume(":") ? readType(reader) : undefined;
|
|
176
|
+
if (returnType === undefined && kind === "property") {
|
|
177
|
+
reader.fail(`${header.value} requires a return type`);
|
|
178
|
+
}
|
|
179
|
+
// Exported declarations may terminate the header itself with a semicolon.
|
|
180
|
+
readDeclarationSemicolon(reader);
|
|
181
|
+
readDeclarationSections(reader);
|
|
182
|
+
return {
|
|
183
|
+
kind,
|
|
184
|
+
name,
|
|
185
|
+
...(returnType === undefined ? {} : { returnType }),
|
|
186
|
+
...splitObject(reader, header, kind, options),
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** Parse only the object envelope and declarations. CODESYS validates executable ST. */
|
|
191
|
+
export const parseSource = (source: string, options: ParseOptions = {}): ParsedSource => {
|
|
192
|
+
const reader = createTokenReader(source, options.filename ?? "<source>");
|
|
193
|
+
readDeclarationPragmas(reader);
|
|
194
|
+
const header = reader.peek();
|
|
195
|
+
if (header?.value === "TYPE") {
|
|
196
|
+
return parseDut(reader);
|
|
197
|
+
}
|
|
198
|
+
if (header && globalSections.has(header.value)) {
|
|
199
|
+
return parseGlobals(reader, options);
|
|
200
|
+
}
|
|
201
|
+
return parsePou(reader, options);
|
|
202
|
+
};
|
package/src/st/reader.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { StSyntaxError, type Token, tokenize } from "./lexer";
|
|
2
|
+
|
|
3
|
+
/** Traverse tokens without losing the source offsets needed for an exact split. */
|
|
4
|
+
export const createTokenReader = (source: string, filename: string) => {
|
|
5
|
+
const tokens = tokenize(source, filename);
|
|
6
|
+
let index = 0;
|
|
7
|
+
const peek = (ahead = 0) => tokens[index + ahead];
|
|
8
|
+
const fail = (message: string, token = peek()): never => {
|
|
9
|
+
throw new StSyntaxError(source, token?.start ?? source.length, filename, message);
|
|
10
|
+
};
|
|
11
|
+
const take = (): Token => {
|
|
12
|
+
const token = peek();
|
|
13
|
+
if (!token) {
|
|
14
|
+
return fail("Unexpected end of declaration");
|
|
15
|
+
}
|
|
16
|
+
index++;
|
|
17
|
+
return token;
|
|
18
|
+
};
|
|
19
|
+
const expect = (value: string) => {
|
|
20
|
+
if (peek()?.value !== value) {
|
|
21
|
+
fail(`Expected ${value}`);
|
|
22
|
+
}
|
|
23
|
+
return take();
|
|
24
|
+
};
|
|
25
|
+
const consume = (value: string) => {
|
|
26
|
+
if (peek()?.value === value) {
|
|
27
|
+
return take();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
source,
|
|
33
|
+
filename,
|
|
34
|
+
tokens,
|
|
35
|
+
peek,
|
|
36
|
+
take,
|
|
37
|
+
expect,
|
|
38
|
+
consume,
|
|
39
|
+
fail,
|
|
40
|
+
get index() {
|
|
41
|
+
return index;
|
|
42
|
+
},
|
|
43
|
+
// The declaration ends at the last consumed token, before any body whitespace.
|
|
44
|
+
get end() {
|
|
45
|
+
return tokens[index - 1]?.end ?? 0;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type TokenReader = ReturnType<typeof createTokenReader>;
|