@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/python/worker.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""One long-lived script inside CODESYS; all engine calls stay on its script thread."""
|
|
2
|
+
from __future__ import print_function
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.environ["CODESYS_BUILD_SESSION"])
|
|
13
|
+
from build import CompilerError, build
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_json(path, value):
|
|
17
|
+
# The client reads only complete replies. Each request has a unique filename.
|
|
18
|
+
with io.open(path + ".tmp", "w", encoding="utf-8") as output:
|
|
19
|
+
output.write(u"{0}".format(json.dumps(value, ensure_ascii=False)))
|
|
20
|
+
os.rename(path + ".tmp", path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run_build(engine, directory, loaded):
|
|
24
|
+
root = os.path.join(os.path.dirname(directory), "build")
|
|
25
|
+
with io.open(os.path.join(root, "request.json"), "r", encoding="utf-8") as source:
|
|
26
|
+
request = json.load(source)
|
|
27
|
+
for key in ("template", "project", "output", "receipt"):
|
|
28
|
+
request[key] = os.path.join(root, request[key])
|
|
29
|
+
try:
|
|
30
|
+
build(engine, request, loaded, directory)
|
|
31
|
+
except Exception:
|
|
32
|
+
with io.open(os.path.join(root, "error.txt"), "w", encoding="utf-8") as output:
|
|
33
|
+
output.write(u"{0}".format(traceback.format_exc()))
|
|
34
|
+
raise
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def serve(engine, directory):
|
|
38
|
+
request_path = os.path.join(directory, "request.json")
|
|
39
|
+
loaded = {}
|
|
40
|
+
while True:
|
|
41
|
+
if not os.path.isfile(request_path):
|
|
42
|
+
# Unlike time.sleep, this keeps CODESYS's message loop responsive.
|
|
43
|
+
engine.system.delay(200)
|
|
44
|
+
continue
|
|
45
|
+
with io.open(request_path, "r", encoding="utf-8") as source:
|
|
46
|
+
command = json.load(source)
|
|
47
|
+
os.remove(request_path)
|
|
48
|
+
response = {"id": command["id"], "ok": True}
|
|
49
|
+
try:
|
|
50
|
+
if command["action"] == "build":
|
|
51
|
+
run_build(engine, directory, loaded)
|
|
52
|
+
elif command["action"] == "online":
|
|
53
|
+
from codesys_online import run_online
|
|
54
|
+
if loaded.get("project") is not None:
|
|
55
|
+
loaded["project"].close()
|
|
56
|
+
loaded.clear()
|
|
57
|
+
root = os.path.join(os.path.dirname(directory), "controller")
|
|
58
|
+
with io.open(os.path.join(root, "request.json"), encoding="utf-8") as source:
|
|
59
|
+
request = json.load(source)
|
|
60
|
+
write_json(os.path.join(root, "result.json"), run_online(engine, request))
|
|
61
|
+
elif command["action"] not in ("ping", "stop"):
|
|
62
|
+
raise ValueError("Unknown worker action")
|
|
63
|
+
except Exception as error:
|
|
64
|
+
response = {"id": command["id"], "ok": False, "error": traceback.format_exc()}
|
|
65
|
+
if isinstance(error, CompilerError):
|
|
66
|
+
response["diagnostics"] = error.diagnostics
|
|
67
|
+
write_json(os.path.join(directory, command["id"] + ".json"), response)
|
|
68
|
+
if command["action"] == "stop":
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
import scriptengine
|
|
74
|
+
scriptengine.system.script_prompt_handling = scriptengine.ScriptPromptHandling.LogPrompts
|
|
75
|
+
native_exit = None
|
|
76
|
+
if os.environ.get("CODESYS_BUILD_NATIVE_EXIT") == "true":
|
|
77
|
+
# Import and bind while the engine is starting. A late IronPython import
|
|
78
|
+
# after serving builds can fault in the runtime's debug-info rewriter.
|
|
79
|
+
import ctypes
|
|
80
|
+
native_exit = ctypes.windll.kernel32.ExitProcess
|
|
81
|
+
exit_code = 0
|
|
82
|
+
try:
|
|
83
|
+
serve(scriptengine, os.environ["CODESYS_BUILD_SESSION"])
|
|
84
|
+
except Exception:
|
|
85
|
+
traceback.print_exc()
|
|
86
|
+
exit_code = 1
|
|
87
|
+
if native_exit is not None:
|
|
88
|
+
native_exit(exit_code)
|
|
89
|
+
scriptengine.system.exit(exit_code)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
application_name=$1
|
|
5
|
+
source_application=${2:-Application}
|
|
6
|
+
config_dir=/conf/codesyscontrol
|
|
7
|
+
data_dir=/data/codesyscontrol
|
|
8
|
+
config_file=$config_dir/CODESYSControl_User.cfg
|
|
9
|
+
|
|
10
|
+
test -d "$config_dir"
|
|
11
|
+
test -d "$data_dir"
|
|
12
|
+
test -s "/codesys-build/$source_application.app"
|
|
13
|
+
test -s "/codesys-build/$source_application.crc"
|
|
14
|
+
|
|
15
|
+
# Match the Virtual Control image's first-start initialization before installing
|
|
16
|
+
# the application. Existing configuration, device users, licenses and data survive.
|
|
17
|
+
if [ ! -f "$config_dir/.docker_initialized" ]; then
|
|
18
|
+
for file in CODESYSControl.cfg CODESYSControl_User.cfg; do
|
|
19
|
+
if [ ! -f "$config_dir/$file" ]; then
|
|
20
|
+
cp "/etc/codesyscontrol/$file" "$config_dir/$file"
|
|
21
|
+
fi
|
|
22
|
+
done
|
|
23
|
+
touch "$config_dir/.docker_initialized"
|
|
24
|
+
fi
|
|
25
|
+
if [ ! -f "$data_dir/.docker_initialized" ]; then
|
|
26
|
+
cp -a /var/opt/codesys/. "$data_dir/"
|
|
27
|
+
touch "$data_dir/.docker_initialized"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Register the boot application once, retaining other applications and settings.
|
|
31
|
+
# Read the full section first so a new application index cannot overwrite one.
|
|
32
|
+
cp -p "$config_file" "$config_file.codesys-build.tmp"
|
|
33
|
+
awk -v application="$application_name" '
|
|
34
|
+
{
|
|
35
|
+
if (FNR == 1) in_app = 0
|
|
36
|
+
if (FILENAME == ARGV[2]) lines[++count] = $0
|
|
37
|
+
line = $0
|
|
38
|
+
sub(/\r$/, "", line)
|
|
39
|
+
gsub(/^[ \t]+|[ \t]+$/, "", line)
|
|
40
|
+
if (line ~ /^\[/) in_app = (line == "[CmpApp]")
|
|
41
|
+
if (in_app && line ~ /^Application\.[0-9]+[ \t]*=/) {
|
|
42
|
+
split(line, parts, "=")
|
|
43
|
+
index_text = parts[1]
|
|
44
|
+
sub(/^Application\./, "", index_text)
|
|
45
|
+
if (index_text + 0 > maximum) maximum = index_text + 0
|
|
46
|
+
value = parts[2]
|
|
47
|
+
gsub(/^[ \t]+|[ \t]+$/, "", value)
|
|
48
|
+
applications[index_text + 0] = value
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
END {
|
|
52
|
+
# User configuration overrides the base file for the same Application.N key.
|
|
53
|
+
for (application_index in applications) {
|
|
54
|
+
if (applications[application_index] == application) registered = 1
|
|
55
|
+
}
|
|
56
|
+
for (i = 1; i <= count; i++) {
|
|
57
|
+
print lines[i]
|
|
58
|
+
if (!registered && lines[i] ~ /^[ \t]*\[CmpApp\][ \t]*\r?$/) {
|
|
59
|
+
print "Application." (maximum + 1) "=" application
|
|
60
|
+
registered = 1
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!registered) print "\n[CmpApp]\nApplication." (maximum + 1) "=" application
|
|
64
|
+
}' "$config_dir/CODESYSControl.cfg" "$config_file" > "$config_file.codesys-build.tmp"
|
|
65
|
+
mv "$config_file.codesys-build.tmp" "$config_file"
|
|
66
|
+
|
|
67
|
+
# The runtime stays stopped until both boot files are installed. Keep retain files
|
|
68
|
+
# and application-owned data; this is a cold deployment, not an online change.
|
|
69
|
+
application_dir=$data_dir/PlcLogic/$application_name
|
|
70
|
+
mkdir -p "$application_dir"
|
|
71
|
+
for extension in app crc; do
|
|
72
|
+
cp "/codesys-build/$source_application.$extension" "$application_dir/$application_name.$extension.tmp"
|
|
73
|
+
mv "$application_dir/$application_name.$extension.tmp" "$application_dir/$application_name.$extension"
|
|
74
|
+
done
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<project xmlns="http://www.plcopen.org/xml/tc6_0200" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
|
3
|
+
<fileHeader companyName="" productName="CODESYS" productVersion="CODESYS V3.5 SP21" creationDateTime="<NORMALIZED>" />
|
|
4
|
+
<contentHeader name="FB_Simple" modificationDateTime="<NORMALIZED>">
|
|
5
|
+
<coordinateInfo>
|
|
6
|
+
<fbd><scaling x="1" y="1" /></fbd>
|
|
7
|
+
<ld><scaling x="1" y="1" /></ld>
|
|
8
|
+
<sfc><scaling x="1" y="1" /></sfc>
|
|
9
|
+
</coordinateInfo>
|
|
10
|
+
</contentHeader>
|
|
11
|
+
<types>
|
|
12
|
+
<dataTypes />
|
|
13
|
+
<pous>
|
|
14
|
+
<pou name="FB_Simple" pouType="functionBlock">
|
|
15
|
+
<interface>
|
|
16
|
+
<inputVars>
|
|
17
|
+
<variable name="bEnable">
|
|
18
|
+
<type><BOOL /></type>
|
|
19
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Enable command</xhtml></documentation>
|
|
20
|
+
</variable>
|
|
21
|
+
</inputVars>
|
|
22
|
+
<outputVars>
|
|
23
|
+
<variable name="bDone">
|
|
24
|
+
<type><BOOL /></type>
|
|
25
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Done flag</xhtml></documentation>
|
|
26
|
+
</variable>
|
|
27
|
+
</outputVars>
|
|
28
|
+
</interface>
|
|
29
|
+
<body>
|
|
30
|
+
<ST>
|
|
31
|
+
<xhtml xmlns="http://www.w3.org/1999/xhtml"><![CDATA[bDone := bEnable;]]></xhtml>
|
|
32
|
+
</ST>
|
|
33
|
+
</body>
|
|
34
|
+
</pou>
|
|
35
|
+
</pous>
|
|
36
|
+
</types>
|
|
37
|
+
<instances>
|
|
38
|
+
<configurations />
|
|
39
|
+
</instances>
|
|
40
|
+
</project>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<project xmlns="http://www.plcopen.org/xml/tc6_0200" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
|
3
|
+
<fileHeader companyName="" productName="CODESYS" productVersion="CODESYS V3.5 SP21" creationDateTime="<NORMALIZED>" />
|
|
4
|
+
<contentHeader name="FB_StateMachine" modificationDateTime="<NORMALIZED>">
|
|
5
|
+
<coordinateInfo>
|
|
6
|
+
<fbd><scaling x="1" y="1" /></fbd>
|
|
7
|
+
<ld><scaling x="1" y="1" /></ld>
|
|
8
|
+
<sfc><scaling x="1" y="1" /></sfc>
|
|
9
|
+
</coordinateInfo>
|
|
10
|
+
</contentHeader>
|
|
11
|
+
<types>
|
|
12
|
+
<dataTypes />
|
|
13
|
+
<pous>
|
|
14
|
+
<pou name="FB_StateMachine" pouType="functionBlock">
|
|
15
|
+
<interface>
|
|
16
|
+
<inputVars>
|
|
17
|
+
<variable name="bExecute">
|
|
18
|
+
<type><BOOL /></type>
|
|
19
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Rising edge starts cycle</xhtml></documentation>
|
|
20
|
+
</variable>
|
|
21
|
+
<variable name="tTimeout">
|
|
22
|
+
<type><TIME /></type>
|
|
23
|
+
<initialValue><simpleValue value="T#5s" /></initialValue>
|
|
24
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Operation timeout</xhtml></documentation>
|
|
25
|
+
</variable>
|
|
26
|
+
</inputVars>
|
|
27
|
+
<outputVars>
|
|
28
|
+
<variable name="bDone">
|
|
29
|
+
<type><BOOL /></type>
|
|
30
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Cycle complete</xhtml></documentation>
|
|
31
|
+
</variable>
|
|
32
|
+
<variable name="bBusy">
|
|
33
|
+
<type><BOOL /></type>
|
|
34
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Cycle in progress</xhtml></documentation>
|
|
35
|
+
</variable>
|
|
36
|
+
<variable name="bErr">
|
|
37
|
+
<type><BOOL /></type>
|
|
38
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Cycle errored</xhtml></documentation>
|
|
39
|
+
</variable>
|
|
40
|
+
<variable name="nErrId">
|
|
41
|
+
<type><INT /></type>
|
|
42
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Error ID</xhtml></documentation>
|
|
43
|
+
</variable>
|
|
44
|
+
</outputVars>
|
|
45
|
+
<localVars>
|
|
46
|
+
<variable name="nState">
|
|
47
|
+
<type><INT /></type>
|
|
48
|
+
<initialValue><simpleValue value="0" /></initialValue>
|
|
49
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">State machine pointer</xhtml></documentation>
|
|
50
|
+
</variable>
|
|
51
|
+
<variable name="fbTimer">
|
|
52
|
+
<type><derived name="TON" /></type>
|
|
53
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Timeout watchdog</xhtml></documentation>
|
|
54
|
+
</variable>
|
|
55
|
+
</localVars>
|
|
56
|
+
</interface>
|
|
57
|
+
<body>
|
|
58
|
+
<ST>
|
|
59
|
+
<xhtml xmlns="http://www.w3.org/1999/xhtml"><![CDATA[CASE nState OF
|
|
60
|
+
0: (* IDLE *)
|
|
61
|
+
IF bExecute THEN
|
|
62
|
+
nState := 10;
|
|
63
|
+
END_IF
|
|
64
|
+
10: (* WORKING *)
|
|
65
|
+
bBusy := TRUE;
|
|
66
|
+
fbTimer(IN := TRUE, PT := tTimeout);
|
|
67
|
+
IF fbTimer.Q THEN
|
|
68
|
+
nState := 99;
|
|
69
|
+
nErrId := 100;
|
|
70
|
+
END_IF
|
|
71
|
+
90: (* DONE *)
|
|
72
|
+
bDone := TRUE;
|
|
73
|
+
bBusy := FALSE;
|
|
74
|
+
99: (* ERROR *)
|
|
75
|
+
bErr := TRUE;
|
|
76
|
+
bBusy := FALSE;
|
|
77
|
+
END_CASE]]></xhtml>
|
|
78
|
+
</ST>
|
|
79
|
+
</body>
|
|
80
|
+
</pou>
|
|
81
|
+
</pous>
|
|
82
|
+
</types>
|
|
83
|
+
<instances>
|
|
84
|
+
<configurations />
|
|
85
|
+
</instances>
|
|
86
|
+
</project>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<project xmlns="http://www.plcopen.org/xml/tc6_0200" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
|
3
|
+
<fileHeader companyName="" productName="CODESYS" productVersion="CODESYS V3.5 SP21" creationDateTime="<NORMALIZED>" />
|
|
4
|
+
<contentHeader name="PRG_Main" modificationDateTime="<NORMALIZED>">
|
|
5
|
+
<coordinateInfo>
|
|
6
|
+
<fbd><scaling x="1" y="1" /></fbd>
|
|
7
|
+
<ld><scaling x="1" y="1" /></ld>
|
|
8
|
+
<sfc><scaling x="1" y="1" /></sfc>
|
|
9
|
+
</coordinateInfo>
|
|
10
|
+
</contentHeader>
|
|
11
|
+
<types>
|
|
12
|
+
<dataTypes />
|
|
13
|
+
<pous>
|
|
14
|
+
<pou name="PRG_Main" pouType="program">
|
|
15
|
+
<interface>
|
|
16
|
+
<localVars>
|
|
17
|
+
<variable name="fbWorker">
|
|
18
|
+
<type><derived name="FB_StateMachine" /></type>
|
|
19
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Cycle worker</xhtml></documentation>
|
|
20
|
+
</variable>
|
|
21
|
+
<variable name="bStart">
|
|
22
|
+
<type><BOOL /></type>
|
|
23
|
+
<documentation><xhtml xmlns="http://www.w3.org/1999/xhtml">Operator start button</xhtml></documentation>
|
|
24
|
+
</variable>
|
|
25
|
+
</localVars>
|
|
26
|
+
</interface>
|
|
27
|
+
<body>
|
|
28
|
+
<ST>
|
|
29
|
+
<xhtml xmlns="http://www.w3.org/1999/xhtml"><![CDATA[fbWorker(
|
|
30
|
+
bExecute := bStart,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
IF fbWorker.bDone THEN
|
|
34
|
+
bStart := FALSE;
|
|
35
|
+
END_IF]]></xhtml>
|
|
36
|
+
</ST>
|
|
37
|
+
</body>
|
|
38
|
+
</pou>
|
|
39
|
+
</pous>
|
|
40
|
+
</types>
|
|
41
|
+
<instances>
|
|
42
|
+
<configurations />
|
|
43
|
+
</instances>
|
|
44
|
+
</project>
|
package/src/artifacts.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { copyFile, cp, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { cyclicTasksSchema } from "./config";
|
|
5
|
+
|
|
6
|
+
export const buildFiles = {
|
|
7
|
+
project: "Application.project",
|
|
8
|
+
output: "runtime/Application.app",
|
|
9
|
+
checksum: "runtime/Application.crc",
|
|
10
|
+
receipt: "build.json",
|
|
11
|
+
// Normalized EDS files of a generated device tree, imported by the worker.
|
|
12
|
+
devices: "eds",
|
|
13
|
+
// With a generated device tree: the project plus the descriptions an IDE needs.
|
|
14
|
+
archive: "Application.projectarchive",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
const receiptSchema = z.object({
|
|
18
|
+
mode: z.literal("build"),
|
|
19
|
+
application: z.string().regex(/^[A-Za-z_][A-Za-z_0-9]*$/),
|
|
20
|
+
tasks: cyclicTasksSchema.optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** A receipt is published last, after every artifact has been checked and replaced. */
|
|
24
|
+
export const readBuild = async (directory: string) => {
|
|
25
|
+
const receipt = receiptSchema.safeParse(
|
|
26
|
+
JSON.parse(await readFile(resolve(directory, buildFiles.receipt), "utf8")),
|
|
27
|
+
);
|
|
28
|
+
if (!receipt.success) {
|
|
29
|
+
throw new Error("CODESYS did not produce a valid build receipt", {
|
|
30
|
+
cause: receipt.error,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
for (const file of [buildFiles.project, buildFiles.output, buildFiles.checksum]) {
|
|
34
|
+
const artifact = await stat(resolve(directory, file));
|
|
35
|
+
if (!artifact.isFile() || artifact.size === 0) {
|
|
36
|
+
throw new Error(`CODESYS produced an empty artifact: ${file}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
...receipt.data,
|
|
41
|
+
directory,
|
|
42
|
+
project: resolve(directory, buildFiles.project),
|
|
43
|
+
output: resolve(directory, buildFiles.output),
|
|
44
|
+
checksum: resolve(directory, buildFiles.checksum),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Publish a verified IDE build, with the completion receipt written last. */
|
|
49
|
+
export const publishBuild = async (
|
|
50
|
+
compiled: Awaited<ReturnType<typeof readBuild>>,
|
|
51
|
+
outDir: string,
|
|
52
|
+
) => {
|
|
53
|
+
await copyFile(compiled.project, resolve(outDir, buildFiles.project));
|
|
54
|
+
await rm(resolve(outDir, "runtime"), { recursive: true, force: true });
|
|
55
|
+
await rename(resolve(compiled.directory, "runtime"), resolve(outDir, "runtime"));
|
|
56
|
+
await rm(resolve(outDir, buildFiles.archive), { force: true });
|
|
57
|
+
await cp(
|
|
58
|
+
resolve(compiled.directory, buildFiles.archive),
|
|
59
|
+
resolve(outDir, buildFiles.archive),
|
|
60
|
+
).catch((cause: unknown) => {
|
|
61
|
+
if (!(cause && typeof cause === "object" && "code" in cause && cause.code === "ENOENT"))
|
|
62
|
+
throw cause;
|
|
63
|
+
});
|
|
64
|
+
await writeFile(
|
|
65
|
+
resolve(outDir, buildFiles.receipt),
|
|
66
|
+
JSON.stringify(
|
|
67
|
+
{
|
|
68
|
+
mode: "build",
|
|
69
|
+
application: compiled.application,
|
|
70
|
+
...(compiled.tasks && { tasks: compiled.tasks }),
|
|
71
|
+
},
|
|
72
|
+
null,
|
|
73
|
+
2,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
return readBuild(outDir);
|
|
77
|
+
};
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access, copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { buildFiles, publishBuild, readBuild } from "./artifacts";
|
|
5
|
+
import { dockerImage, type ResolvedConfig } from "./config";
|
|
6
|
+
import { buildInDocker } from "./docker-build";
|
|
7
|
+
import { createCodesysWorker, withWorkerLock } from "./host/codesys-worker";
|
|
8
|
+
import { loadSources } from "./sources";
|
|
9
|
+
import { toWindowsPath } from "./host/windows-path.ts";
|
|
10
|
+
|
|
11
|
+
export const buildProject = async (config: ResolvedConfig) => {
|
|
12
|
+
if (!config.docker) throw new Error("Builds require a Docker builder image.");
|
|
13
|
+
return buildInDocker(config, dockerImage);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** The container runner owns Wine; this operation is not a public host build. */
|
|
17
|
+
export const buildInContainer = async (config: ResolvedConfig) => {
|
|
18
|
+
return withWorkerLock(config, async () => {
|
|
19
|
+
// Invalidate the previous build before validation, so run cannot deploy stale
|
|
20
|
+
// output after a failed rebuild. Keep the last project available for inspection.
|
|
21
|
+
await rm(resolve(config.outDir, buildFiles.receipt), { force: true });
|
|
22
|
+
const sources = await loadSources(config);
|
|
23
|
+
await access(config.template);
|
|
24
|
+
const worker = createCodesysWorker(config);
|
|
25
|
+
const session = await worker.launch();
|
|
26
|
+
const directory = resolve(config.outDir, ".codesys/build");
|
|
27
|
+
await rm(directory, { recursive: true, force: true });
|
|
28
|
+
await mkdir(resolve(directory, "runtime"), { recursive: true });
|
|
29
|
+
const template = resolve(directory, "Template.project");
|
|
30
|
+
await copyFile(config.template, template);
|
|
31
|
+
// CODESYS imports each adapter's EDS from the build directory.
|
|
32
|
+
const deviceTree = config.deviceTree && {
|
|
33
|
+
...config.deviceTree,
|
|
34
|
+
devices: await Promise.all(
|
|
35
|
+
config.deviceTree.devices.map(async (device) => {
|
|
36
|
+
const eds = `${buildFiles.devices}/${device.name}.eds`;
|
|
37
|
+
await mkdir(resolve(directory, buildFiles.devices), { recursive: true });
|
|
38
|
+
await writeFile(resolve(directory, eds), device.eds);
|
|
39
|
+
// The digest keys project reuse on the description's content.
|
|
40
|
+
return { ...device, eds, sha256: createHash("sha256").update(device.eds).digest("hex") };
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
const request = {
|
|
45
|
+
libraryReferences: config.libraryReferences,
|
|
46
|
+
libraries: await Promise.all(
|
|
47
|
+
(config.libraries ?? []).map(async (library) => ({
|
|
48
|
+
...library,
|
|
49
|
+
path: await toWindowsPath(library.path, config.codesys.wine),
|
|
50
|
+
})),
|
|
51
|
+
),
|
|
52
|
+
...sources,
|
|
53
|
+
task: config.task,
|
|
54
|
+
...(config.tasks && { tasks: config.tasks }),
|
|
55
|
+
removeObjects: config.removeObjects,
|
|
56
|
+
symbols: config.symbols,
|
|
57
|
+
...(deviceTree && { deviceTree }),
|
|
58
|
+
...(config.controller && { controller: config.controller }),
|
|
59
|
+
mode: "build",
|
|
60
|
+
output: buildFiles.output,
|
|
61
|
+
project: buildFiles.project,
|
|
62
|
+
archive: buildFiles.archive,
|
|
63
|
+
receipt: buildFiles.receipt,
|
|
64
|
+
template: "Template.project",
|
|
65
|
+
templateHash: createHash("sha256")
|
|
66
|
+
.update(await readFile(template))
|
|
67
|
+
.digest("hex"),
|
|
68
|
+
};
|
|
69
|
+
await writeFile(resolve(directory, "request.json"), JSON.stringify(request, null, 2));
|
|
70
|
+
console.log(`CODESYS build: ${config.outDir}`);
|
|
71
|
+
try {
|
|
72
|
+
await worker.request(session, { action: "build" }, config.timeouts.build);
|
|
73
|
+
const compiled = await readBuild(directory);
|
|
74
|
+
if (config.tasks && JSON.stringify(compiled.tasks) !== JSON.stringify(config.tasks)) {
|
|
75
|
+
throw new Error("CODESYS build receipt does not match the requested task plan");
|
|
76
|
+
}
|
|
77
|
+
// Builds and runs share the lock. Removing the receipt above keeps a failed
|
|
78
|
+
// publication from looking complete even if only some files were replaced.
|
|
79
|
+
return await publishBuild(compiled, config.outDir);
|
|
80
|
+
} catch (cause) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`CODESYS build failed. Inspect ${directory}/progress.log, error.txt and ${session.directory}/codesys.log`,
|
|
83
|
+
{ cause },
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
};
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { deviceTree } from "@tsplc/typegen";
|
|
4
|
+
import { loadHardware, resolveIo } from "@tsplc/typegen/hardware";
|
|
5
|
+
import { controllerFor, loadTsplcConfig, rawBuildConfig } from "@tsplc/config";
|
|
6
|
+
import { buildProject } from "./build.ts";
|
|
7
|
+
import { resolveConfig } from "./config.ts";
|
|
8
|
+
import { inspectController, updateController } from "./controller-online.ts";
|
|
9
|
+
import { configureDocker, configureRuntime } from "./docker-build.ts";
|
|
10
|
+
import { installLibraries } from "./libraries.ts";
|
|
11
|
+
import { runProject } from "./run.ts";
|
|
12
|
+
import { loadSources } from "./sources.ts";
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const { values, positionals } = parseArgs({
|
|
16
|
+
allowPositionals: true,
|
|
17
|
+
options: {
|
|
18
|
+
config: { type: "string" },
|
|
19
|
+
help: { type: "boolean", short: "h" },
|
|
20
|
+
read: { type: "string", multiple: true },
|
|
21
|
+
"initialize-user": { type: "boolean" },
|
|
22
|
+
"frozen-lockfile": { type: "boolean" },
|
|
23
|
+
runtime: { type: "boolean" },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const command = positionals[0];
|
|
27
|
+
const commands = new Set([
|
|
28
|
+
"configure",
|
|
29
|
+
"install",
|
|
30
|
+
"check",
|
|
31
|
+
"build",
|
|
32
|
+
"run",
|
|
33
|
+
"download",
|
|
34
|
+
"online-change",
|
|
35
|
+
"inspect",
|
|
36
|
+
]);
|
|
37
|
+
if (positionals.length > 1) throw new Error("Specify one command.");
|
|
38
|
+
if (!command || values.help) {
|
|
39
|
+
console.log(
|
|
40
|
+
"tsplc-build configure|install|check|build|run|download|online-change|inspect [--config tsplc.config.ts] [--read symbol] [--runtime]",
|
|
41
|
+
);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!commands.has(command)) throw new Error(`Unknown command ${command}.`);
|
|
45
|
+
if (values.runtime && command !== "configure")
|
|
46
|
+
throw new Error("--runtime applies only to configure.");
|
|
47
|
+
if (command === "configure") {
|
|
48
|
+
await (values.runtime ? configureRuntime() : configureDocker());
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const { config, configFile, root } = await loadTsplcConfig(values.config);
|
|
52
|
+
if (command === "install") {
|
|
53
|
+
const libraries = await installLibraries(config.dependencies, root, {
|
|
54
|
+
frozen: values["frozen-lockfile"],
|
|
55
|
+
});
|
|
56
|
+
console.log(`Installed ${libraries.length} CODESYS libraries.`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!config.build) throw new Error("Configure build options in tsplc.config.ts.");
|
|
60
|
+
if (command === "inspect") {
|
|
61
|
+
// Inspection only needs connection/baseline settings; it remains usable
|
|
62
|
+
// without sources, EDS descriptors or installed library payloads.
|
|
63
|
+
console.log(
|
|
64
|
+
JSON.stringify(
|
|
65
|
+
await inspectController(
|
|
66
|
+
resolveConfig(
|
|
67
|
+
{ ...config.build, controller: controllerFor(config, command) },
|
|
68
|
+
configFile,
|
|
69
|
+
),
|
|
70
|
+
values.read,
|
|
71
|
+
),
|
|
72
|
+
null,
|
|
73
|
+
2,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (config.compiler)
|
|
79
|
+
throw new Error("Use tsplc for TypeScript projects. tsplc-build accepts raw Structured Text.");
|
|
80
|
+
const build = rawBuildConfig(config);
|
|
81
|
+
const hardware = await loadHardware(config, root);
|
|
82
|
+
const io = resolveIo(config, hardware);
|
|
83
|
+
if (command === "check") {
|
|
84
|
+
const sources = await loadSources(resolveConfig({ ...build, io }, configFile));
|
|
85
|
+
console.log(`Validated ${sources.objects.length} Structured Text objects.`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const libraries = await installLibraries(config.dependencies, root, {
|
|
89
|
+
frozen: values["frozen-lockfile"],
|
|
90
|
+
});
|
|
91
|
+
const settings = resolveConfig(
|
|
92
|
+
{
|
|
93
|
+
...build,
|
|
94
|
+
libraries,
|
|
95
|
+
io,
|
|
96
|
+
deviceTree: await deviceTree(config, hardware),
|
|
97
|
+
controller: controllerFor(config, command),
|
|
98
|
+
},
|
|
99
|
+
configFile,
|
|
100
|
+
);
|
|
101
|
+
if (command === "build") await buildProject(settings);
|
|
102
|
+
else if (command === "run") {
|
|
103
|
+
await buildProject(settings);
|
|
104
|
+
await runProject(settings, config.name);
|
|
105
|
+
} else if (command === "download" || command === "online-change")
|
|
106
|
+
await updateController(settings, command, {
|
|
107
|
+
initializeUser: values["initialize-user"],
|
|
108
|
+
read: values.read,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
await main();
|
|
113
|
+
} catch (cause) {
|
|
114
|
+
console.error(cause instanceof Error ? cause.message : String(cause));
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
package/src/config.ts
ADDED