phobos-templates-utils 0.0.1__py3-none-any.whl
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.
- phobos_templates_utils-0.0.1.dist-info/METADATA +23 -0
- phobos_templates_utils-0.0.1.dist-info/RECORD +12 -0
- phobos_templates_utils-0.0.1.dist-info/WHEEL +5 -0
- phobos_templates_utils-0.0.1.dist-info/top_level.txt +1 -0
- torizon_templates_utils/__init__.py +0 -0
- torizon_templates_utils/animations.py +67 -0
- torizon_templates_utils/args.py +83 -0
- torizon_templates_utils/colors.py +56 -0
- torizon_templates_utils/debug.py +22 -0
- torizon_templates_utils/errors.py +58 -0
- torizon_templates_utils/network.py +35 -0
- torizon_templates_utils/tasks.py +941 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phobos-templates-utils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Package with utilities for Phobos Templates scripts
|
|
5
|
+
Author-email: Matheus Castello <matheus@castello.eng.br>
|
|
6
|
+
Project-URL: Homepage, https://github.com/gaiaBuildSystem/vscode-phobos-templates
|
|
7
|
+
Project-URL: Issues, https://github.com/gaiaBuildSystem/vscode-phobos-templates/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: torizon-io-api
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: pyyaml
|
|
16
|
+
Requires-Dist: debugpy
|
|
17
|
+
Requires-Dist: pkginfo>=1.12
|
|
18
|
+
|
|
19
|
+
# Phobos Templates Utils
|
|
20
|
+
|
|
21
|
+
This package contains a set of utilities to help the scripts that generate the Torizon OS applications and containers.
|
|
22
|
+
|
|
23
|
+
If you want to know more about check [Toradex developer website](https://developer.toradex.com/torizon/application-development/ide-extension/).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
torizon_templates_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
torizon_templates_utils/animations.py,sha256=06THXo79OilHogufkSPTsAVORFzZKJwvRNKHaw6bOTc,1841
|
|
3
|
+
torizon_templates_utils/args.py,sha256=nN1b9Ts2lEYbpAGzaMWpAcmcYIrTUFJHehM_UqvnCWA,2142
|
|
4
|
+
torizon_templates_utils/colors.py,sha256=Y1FZ0HtKvXfLGYTDclsXubtZCQ-epuch5Dgk1DJdjps,1143
|
|
5
|
+
torizon_templates_utils/debug.py,sha256=OfdKSdN1kypuJnV_WcPa9bvonjyDvXvA3QTmc1iKyjw,556
|
|
6
|
+
torizon_templates_utils/errors.py,sha256=Wkk0AeCx0KQW-Qlu_5A6GYaGUkaJU79Mok8lZuZKHjM,1425
|
|
7
|
+
torizon_templates_utils/network.py,sha256=NpBeJlvpCY0Gvdc8ZzCKfmAk6zGYD_gLx-rTzZn9ZsU,1528
|
|
8
|
+
torizon_templates_utils/tasks.py,sha256=_Kzyiln9TjHJ8D1JNTzW_a-9thcDs7dFFtt7oG-jD_g,32815
|
|
9
|
+
phobos_templates_utils-0.0.1.dist-info/METADATA,sha256=TEpXYjRSAK5w-k7tCSyysS-dI8Ci0337b0910qPICB4,982
|
|
10
|
+
phobos_templates_utils-0.0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
11
|
+
phobos_templates_utils-0.0.1.dist-info/top_level.txt,sha256=qlOkS1ASF5C-kSfpbiWbVHJFgmQ0RN6iunmLArNt6zc,24
|
|
12
|
+
phobos_templates_utils-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
torizon_templates_utils
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
from itertools import cycle
|
|
5
|
+
|
|
6
|
+
def run_command_with_wait_animation(call, *args):
|
|
7
|
+
anima_frames = ["🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛"]
|
|
8
|
+
|
|
9
|
+
def animate():
|
|
10
|
+
for frame in cycle(anima_frames):
|
|
11
|
+
if not running[0]:
|
|
12
|
+
break
|
|
13
|
+
sys.stdout.write(f"\r{frame} :: RUNNING PLEASE WAIT :: {frame}")
|
|
14
|
+
sys.stdout.flush()
|
|
15
|
+
time.sleep(0.1)
|
|
16
|
+
|
|
17
|
+
# Clear the line
|
|
18
|
+
sys.stdout.write("\r\x1b[2K")
|
|
19
|
+
|
|
20
|
+
if running[1]:
|
|
21
|
+
sys.stdout.write("❌ :: TASK FAILED :: ❌\n")
|
|
22
|
+
else:
|
|
23
|
+
sys.stdout.write("✅ :: TASK COMPLETED :: ✅\n")
|
|
24
|
+
|
|
25
|
+
# clear anything the command printed below the animation line
|
|
26
|
+
# while it was pinned (e.g. the scan live device list)
|
|
27
|
+
sys.stdout.write("\x1b[J")
|
|
28
|
+
sys.stdout.flush()
|
|
29
|
+
|
|
30
|
+
def target():
|
|
31
|
+
nonlocal output
|
|
32
|
+
try:
|
|
33
|
+
output = call(*args)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
output = e
|
|
36
|
+
running[1] = True
|
|
37
|
+
finally:
|
|
38
|
+
running[0] = False
|
|
39
|
+
|
|
40
|
+
# [0] is if it's running [1] if it has failed
|
|
41
|
+
running = [True, False]
|
|
42
|
+
output = None
|
|
43
|
+
|
|
44
|
+
animation_thread = threading.Thread(target=animate)
|
|
45
|
+
command_thread = threading.Thread(target=target)
|
|
46
|
+
|
|
47
|
+
animation_thread.start()
|
|
48
|
+
command_thread.start()
|
|
49
|
+
|
|
50
|
+
command_thread.join()
|
|
51
|
+
animation_thread.join()
|
|
52
|
+
|
|
53
|
+
if isinstance(output, Exception):
|
|
54
|
+
raise output
|
|
55
|
+
|
|
56
|
+
return output
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# # Example usage
|
|
60
|
+
# def example_script(duration):
|
|
61
|
+
# time.sleep(duration)
|
|
62
|
+
# return "Task finished"
|
|
63
|
+
|
|
64
|
+
# if __name__ == "__main__":
|
|
65
|
+
# print("LET'S RUN A SCRIPT THAT TAKES 5 SECONDS TO FINISH")
|
|
66
|
+
# result = run_command_in_background_with_wait_animation(example_script, 5)
|
|
67
|
+
# print(result)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
from typing import TypeVar, Type
|
|
4
|
+
from torizon_templates_utils.errors import Error, Error_Out
|
|
5
|
+
|
|
6
|
+
T = TypeVar('T')
|
|
7
|
+
|
|
8
|
+
def get_arg_not_empty(index: int) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Get an argument from the command line.
|
|
11
|
+
If the argument is an empty string, an error is raised.
|
|
12
|
+
"""
|
|
13
|
+
_arg = sys.argv[index]
|
|
14
|
+
|
|
15
|
+
if _arg == "":
|
|
16
|
+
Error_Out(
|
|
17
|
+
"Error: Argument cannot be empty",
|
|
18
|
+
Error.EUSER
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return _arg
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_optional_arg(index: int, default: T) -> T | bool | str:
|
|
25
|
+
"""
|
|
26
|
+
Get an optional argument from the command line.
|
|
27
|
+
If the argument is not provided, the default value is returned.
|
|
28
|
+
"""
|
|
29
|
+
if len(sys.argv) > index:
|
|
30
|
+
# sys.argv return string
|
|
31
|
+
# we need to return T
|
|
32
|
+
# FIXME: this only convert string to bool for now
|
|
33
|
+
if type(default) is bool:
|
|
34
|
+
if sys.argv[index] == "True" or sys.argv[index] == "true" or sys.argv[index] == "1":
|
|
35
|
+
return True
|
|
36
|
+
else:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
return sys.argv[index]
|
|
40
|
+
|
|
41
|
+
return default
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_arg_iterative(
|
|
45
|
+
index: int, prompt: str, default_type: Type, default: T | None = None, iterative: bool = False
|
|
46
|
+
) -> T | None | bool | str:
|
|
47
|
+
"""
|
|
48
|
+
Get an argument from the command line.
|
|
49
|
+
If the argument is not provided, an error is raised.
|
|
50
|
+
"""
|
|
51
|
+
if len(sys.argv) > index:
|
|
52
|
+
if default_type is bool:
|
|
53
|
+
if sys.argv[index] == "True":
|
|
54
|
+
return True
|
|
55
|
+
else:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
return sys.argv[index]
|
|
59
|
+
elif default != None:
|
|
60
|
+
return default
|
|
61
|
+
elif iterative:
|
|
62
|
+
_input = input(prompt)
|
|
63
|
+
if _input == "":
|
|
64
|
+
Error_Out(
|
|
65
|
+
"Error: Argument cannot be empty",
|
|
66
|
+
Error.EUSER
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
if default_type is bool:
|
|
70
|
+
if _input == "True":
|
|
71
|
+
return True
|
|
72
|
+
else:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
return _input
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
Error_Out(
|
|
79
|
+
f"Error: Argument for prompt [{prompt}] not provided",
|
|
80
|
+
Error.EUSER
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return default
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
|
|
2
|
+
import builtins
|
|
3
|
+
import enum
|
|
4
|
+
|
|
5
|
+
class Color(enum.IntEnum):
|
|
6
|
+
NONE = 0
|
|
7
|
+
BLACK = 30
|
|
8
|
+
RED = 31
|
|
9
|
+
GREEN = 32
|
|
10
|
+
YELLOW = 33
|
|
11
|
+
BLUE = 34
|
|
12
|
+
MAGENTA = 35
|
|
13
|
+
CYAN = 36
|
|
14
|
+
WHITE = 37
|
|
15
|
+
|
|
16
|
+
class BgColor(enum.IntEnum):
|
|
17
|
+
NONE = 0
|
|
18
|
+
BLACK = 40
|
|
19
|
+
RED = 41
|
|
20
|
+
BRIGTH_RED = 101
|
|
21
|
+
GREEN = 42
|
|
22
|
+
BRIGTH_GREEN = 102
|
|
23
|
+
YELLOW = 43
|
|
24
|
+
BRIGTH_YELLOW = 103
|
|
25
|
+
BLUE = 44
|
|
26
|
+
BRIGTH_BLUE = 104
|
|
27
|
+
MAGENTA = 45
|
|
28
|
+
BRIGTH_MAGENTA = 105
|
|
29
|
+
CYAN = 46
|
|
30
|
+
BRIGTH_CYAN = 106
|
|
31
|
+
WHITE = 47
|
|
32
|
+
BRIGTH_WHITE = 107
|
|
33
|
+
|
|
34
|
+
# override print to have color and background color
|
|
35
|
+
def print(
|
|
36
|
+
*args,
|
|
37
|
+
color: Color = Color.NONE,
|
|
38
|
+
bg_color: BgColor = BgColor.NONE,
|
|
39
|
+
**kwargs
|
|
40
|
+
) -> None:
|
|
41
|
+
_color: int = color.value
|
|
42
|
+
_bg_color: int = bg_color.value
|
|
43
|
+
|
|
44
|
+
start_escape = ""
|
|
45
|
+
|
|
46
|
+
if _color != 0 and _bg_color != 0:
|
|
47
|
+
start_escape += f"\033[{_color};{_bg_color}m"
|
|
48
|
+
elif _color != 0:
|
|
49
|
+
start_escape += f"\033[{_color}m"
|
|
50
|
+
elif _bg_color != 0:
|
|
51
|
+
start_escape += f"\033[{_bg_color}m"
|
|
52
|
+
|
|
53
|
+
end_escape = "\033[0m"
|
|
54
|
+
|
|
55
|
+
text = ' '.join(map(str, args))
|
|
56
|
+
builtins.print(f'{start_escape}{text}{end_escape}', **kwargs)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# pylint: disable=missing-function-docstring
|
|
2
|
+
# pylint: disable=missing-module-docstring
|
|
3
|
+
import debugpy # type: ignore[import-untyped]
|
|
4
|
+
|
|
5
|
+
DEBUG_INITIALIZED = False
|
|
6
|
+
|
|
7
|
+
def vscode_prepare(port: int = 5679) -> None:
|
|
8
|
+
global DEBUG_INITIALIZED
|
|
9
|
+
|
|
10
|
+
if DEBUG_INITIALIZED:
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
print("__debugpy__")
|
|
14
|
+
debugpy.listen(("0.0.0.0", port))
|
|
15
|
+
print("__debugpy__ go")
|
|
16
|
+
debugpy.wait_for_client()
|
|
17
|
+
print(f"__debugpy__ is connected [{debugpy.is_client_connected()}]")
|
|
18
|
+
|
|
19
|
+
DEBUG_INITIALIZED = True
|
|
20
|
+
|
|
21
|
+
def breakpoint() -> None:
|
|
22
|
+
debugpy.breakpoint()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from torizon_templates_utils.colors import Color, BgColor, print
|
|
5
|
+
|
|
6
|
+
class _error_struct:
|
|
7
|
+
code: int
|
|
8
|
+
message: str
|
|
9
|
+
|
|
10
|
+
def __init__(self, code: int, message: str):
|
|
11
|
+
self.code = code
|
|
12
|
+
self.message = message
|
|
13
|
+
|
|
14
|
+
# Standard Unix errors. There may be other standard error codes also:
|
|
15
|
+
# https://github.com/kaushalparikh/nuttx/blob/master/include/errno.h
|
|
16
|
+
class Error(Enum):
|
|
17
|
+
ENOCONF = _error_struct(
|
|
18
|
+
1, "Not configured"
|
|
19
|
+
)
|
|
20
|
+
EINVAL = _error_struct(
|
|
21
|
+
22, "Invalid argument"
|
|
22
|
+
)
|
|
23
|
+
ENOPKG = _error_struct(
|
|
24
|
+
65, "Package not installed"
|
|
25
|
+
)
|
|
26
|
+
EUSER = _error_struct(
|
|
27
|
+
69, "User fault"
|
|
28
|
+
)
|
|
29
|
+
EABORT = _error_struct(
|
|
30
|
+
170, "Abort"
|
|
31
|
+
)
|
|
32
|
+
ETASKEXEC = _error_struct(
|
|
33
|
+
310, "Task execution error"
|
|
34
|
+
)
|
|
35
|
+
ENOFOUND = _error_struct(
|
|
36
|
+
404, "Not found"
|
|
37
|
+
)
|
|
38
|
+
EFAIL = _error_struct(
|
|
39
|
+
500, "Failed"
|
|
40
|
+
)
|
|
41
|
+
EUNKNOWN = _error_struct(
|
|
42
|
+
666, "Unknown error"
|
|
43
|
+
)
|
|
44
|
+
ETOMCRUISE = _error_struct(
|
|
45
|
+
999, "Impossible condition"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def Error_Out(msg: str, error: Error) -> None:
|
|
50
|
+
print(f"\n{msg}", color=Color.RED)
|
|
51
|
+
print(f"Error cause: {error.value.message}\n", color=Color.RED)
|
|
52
|
+
sys.exit(error.value.code)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def last_return_code() -> int:
|
|
56
|
+
# we are ignoring the type here because this will get the current
|
|
57
|
+
# xonsh shell instance
|
|
58
|
+
return __xonsh__.last.returncode # type: ignore
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_host_ip():
|
|
8
|
+
if 'WSL_DISTRO_NAME' in os.environ and os.environ['WSL_DISTRO_NAME'] != '':
|
|
9
|
+
command = ["/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe", "-c", '(Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias (Get-NetRoute -DestinationPrefix "0.0.0.0/0").InterfaceAlias).IPAddress']
|
|
10
|
+
|
|
11
|
+
# if inside a container we need to run the command in the host
|
|
12
|
+
if "APOLLOX_CONTAINER" in os.environ:
|
|
13
|
+
command = [
|
|
14
|
+
"sudo", "nsenter", "-t", "1", "-m", "-u", "-n", "-i",
|
|
15
|
+
"--",
|
|
16
|
+
"/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe", "-c", '(Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias (Get-NetRoute -DestinationPrefix "0.0.0.0/0").InterfaceAlias).IPAddress'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
result = subprocess.run(command, capture_output=True, text=True)
|
|
20
|
+
ip_address = re.search(r'((1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\.){3}(1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])', result.stdout)
|
|
21
|
+
if ip_address:
|
|
22
|
+
return ip_address.group(0)
|
|
23
|
+
else:
|
|
24
|
+
command = ["hostname", "-I"]
|
|
25
|
+
|
|
26
|
+
# if inside a container we need to run the command in the host
|
|
27
|
+
if "APOLLOX_CONTAINER" in os.environ:
|
|
28
|
+
command = [
|
|
29
|
+
"sudo", "nsenter", "-t", "1", "-m", "-u", "-n", "-i",
|
|
30
|
+
"--",
|
|
31
|
+
"hostname", "-I"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
result = subprocess.run(command, capture_output=True, text=True)
|
|
35
|
+
return result.stdout.split()[0]
|
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import yaml # type: ignore[import-untyped]
|
|
5
|
+
import json
|
|
6
|
+
import inspect
|
|
7
|
+
import mimetypes
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Dict, Type, TypeVar, Union, Tuple, Optional, Literal
|
|
11
|
+
from torizon_templates_utils.colors import print, Color
|
|
12
|
+
|
|
13
|
+
T = TypeVar('T')
|
|
14
|
+
|
|
15
|
+
def replace_tasks_input():
|
|
16
|
+
for file in Path('.').rglob('*.json'):
|
|
17
|
+
print(file, flush=True)
|
|
18
|
+
mime_type, _ = mimetypes.guess_type(file)
|
|
19
|
+
|
|
20
|
+
if mime_type is None or mime_type.startswith("application/octet-stream"):
|
|
21
|
+
if "id_rsa" not in str(file):
|
|
22
|
+
with open(file, 'r') as f:
|
|
23
|
+
content = f.read()
|
|
24
|
+
|
|
25
|
+
content = content.replace("input:dockerLogin", "command:docker_login")
|
|
26
|
+
content = content.replace("input:dockerImageRegistry", "command:docker_registry")
|
|
27
|
+
content = content.replace("input:dockerPsswd", "command:docker_password")
|
|
28
|
+
|
|
29
|
+
with open(file, 'w') as f:
|
|
30
|
+
f.write(content)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _cast_from_json(json_data, cls: Type[T]) -> T:
|
|
34
|
+
# check on json_data if there is some key with . like "name.prop"
|
|
35
|
+
# if so, we need change the key to something like "name_prop"
|
|
36
|
+
keys = list(json_data.keys())
|
|
37
|
+
for key in keys:
|
|
38
|
+
if '.' in key:
|
|
39
|
+
new_key = key.replace('.', '_')
|
|
40
|
+
json_data[new_key] = json_data.pop(key)
|
|
41
|
+
|
|
42
|
+
expected_args = inspect.signature(cls.__init__).parameters
|
|
43
|
+
filtered_data = {k: v for k, v in json_data.items() if k in expected_args}
|
|
44
|
+
|
|
45
|
+
# check if the cls type has the any attribute
|
|
46
|
+
# the any attribute is a Dict[str, str]
|
|
47
|
+
# and it store the non expected args
|
|
48
|
+
if 'any' in expected_args:
|
|
49
|
+
non_expected_args = {k: v for k, v in json_data.items() if k not in expected_args}
|
|
50
|
+
filtered_data['any'] = non_expected_args
|
|
51
|
+
|
|
52
|
+
return cls(**filtered_data)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# For Settings interface we are mapping only the Torizon specific settings
|
|
56
|
+
class TorizonSettings:
|
|
57
|
+
"""
|
|
58
|
+
TorizonSettings is a interface to map specific VS Code settings defined
|
|
59
|
+
by the Torizon extension.
|
|
60
|
+
"""
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
torizon_psswd: Optional[str] = None,
|
|
64
|
+
torizon_login: Optional[str] = None,
|
|
65
|
+
torizon_ip: Optional[str] = None,
|
|
66
|
+
torizon_ssh_port: Optional[str] = None,
|
|
67
|
+
host_ip: Optional[str] = None,
|
|
68
|
+
torizon_workspace: Optional[str] = None,
|
|
69
|
+
torizon_debug_ssh_port: Optional[str] = None,
|
|
70
|
+
torizon_debug_port1: Optional[str] = None,
|
|
71
|
+
torizon_debug_port2: Optional[str] = None,
|
|
72
|
+
torizon_debug_port3: Optional[str] = None,
|
|
73
|
+
torizon_gpu: Optional[str] = None,
|
|
74
|
+
torizon_arch: Optional[str] = None,
|
|
75
|
+
wait_sync: Optional[str] = None,
|
|
76
|
+
torizon_run_as: Optional[str] = None,
|
|
77
|
+
torizon_app_root: Optional[str] = None,
|
|
78
|
+
docker_tag: Optional[str] = None,
|
|
79
|
+
tcb_packageName: Optional[str] = None,
|
|
80
|
+
tcb_version: Optional[str] = None,
|
|
81
|
+
torizon_gpuPrefixRC: Optional[str] = None,
|
|
82
|
+
any: Optional[Dict[str, str]] = None
|
|
83
|
+
):
|
|
84
|
+
|
|
85
|
+
self.torizon_psswd = torizon_psswd
|
|
86
|
+
self.torizon_login = torizon_login
|
|
87
|
+
self.torizon_ip = torizon_ip
|
|
88
|
+
self.torizon_ssh_port = torizon_ssh_port
|
|
89
|
+
self.host_ip = host_ip
|
|
90
|
+
self.torizon_workspace = torizon_workspace
|
|
91
|
+
self.torizon_debug_ssh_port = torizon_debug_ssh_port
|
|
92
|
+
self.torizon_debug_port1 = torizon_debug_port1
|
|
93
|
+
self.torizon_debug_port2 = torizon_debug_port2
|
|
94
|
+
self.torizon_debug_port3 = torizon_debug_port3
|
|
95
|
+
self.torizon_gpu = torizon_gpu
|
|
96
|
+
self.torizon_arch = torizon_arch
|
|
97
|
+
self.wait_sync = wait_sync
|
|
98
|
+
self.torizon_run_as = torizon_run_as
|
|
99
|
+
self.torizon_app_root = torizon_app_root
|
|
100
|
+
self.docker_tag = docker_tag
|
|
101
|
+
self.tcb_packageName = tcb_packageName
|
|
102
|
+
self.tcb_version = tcb_version
|
|
103
|
+
self.torizon_gpuPrefixRC = torizon_gpuPrefixRC
|
|
104
|
+
self.any = any
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# These are from:
|
|
108
|
+
# https://code.visualstudio.com/docs/editor/tasks-appendix
|
|
109
|
+
|
|
110
|
+
class ShellConfiguration:
|
|
111
|
+
def __init__(self, executable: str, args: Optional[List[str]]):
|
|
112
|
+
self.executable = executable
|
|
113
|
+
self.args = args
|
|
114
|
+
|
|
115
|
+
class CommandOptions:
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
cwd: Optional[str] = None,
|
|
119
|
+
env: Optional[Dict[str, str]] = None,
|
|
120
|
+
shell: Optional[ShellConfiguration] = None
|
|
121
|
+
):
|
|
122
|
+
|
|
123
|
+
self.cwd = cwd
|
|
124
|
+
self.env = env
|
|
125
|
+
self.shell = shell
|
|
126
|
+
|
|
127
|
+
# we are getting this data from json
|
|
128
|
+
# so we need to cast the classes dependencies
|
|
129
|
+
if shell:
|
|
130
|
+
self.shell = _cast_from_json(shell, ShellConfiguration)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class PresentationOptions:
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
reveal: Optional[Literal['never', 'silent', 'always']] = None,
|
|
137
|
+
echo: Optional[bool] = None,
|
|
138
|
+
focus: Optional[bool] = None,
|
|
139
|
+
panel: Optional[Literal['shared', 'dedicated', 'new']] = None,
|
|
140
|
+
showReuseMessage: Optional[bool] = None,
|
|
141
|
+
clear: Optional[bool] = None,
|
|
142
|
+
group: Optional[str] = None
|
|
143
|
+
):
|
|
144
|
+
|
|
145
|
+
self.reveal = reveal
|
|
146
|
+
self.echo = echo
|
|
147
|
+
self.focus = focus
|
|
148
|
+
self.panel = panel
|
|
149
|
+
self.showReuseMessage = showReuseMessage
|
|
150
|
+
self.clear = clear
|
|
151
|
+
self.group = group
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ProblemPattern:
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
regexp: str,
|
|
158
|
+
kind: Optional[Literal['file', 'location']] = None,
|
|
159
|
+
file: Union[int, float] = 0,
|
|
160
|
+
location: Optional[Union[int, float]] = None,
|
|
161
|
+
line: Optional[Union[int, float]] =None,
|
|
162
|
+
column: Optional[Union[int, float]] = None,
|
|
163
|
+
endLine: Optional[Union[int, float]] = None,
|
|
164
|
+
endColumn: Optional[Union[int, float]] = None,
|
|
165
|
+
severity: Optional[Union[int, float]] = None,
|
|
166
|
+
code: Optional[Union[int, float]] = None,
|
|
167
|
+
message: Union[int, float] = 0,
|
|
168
|
+
loop: Optional[bool] = False
|
|
169
|
+
):
|
|
170
|
+
|
|
171
|
+
self.regexp = regexp
|
|
172
|
+
self.kind = kind
|
|
173
|
+
self.file = file
|
|
174
|
+
self.location = location
|
|
175
|
+
self.line = line
|
|
176
|
+
self.column = column
|
|
177
|
+
self.endLine = endLine
|
|
178
|
+
self.endColumn = endColumn
|
|
179
|
+
self.severity = severity
|
|
180
|
+
self.code = code
|
|
181
|
+
self.message = message
|
|
182
|
+
self.loop = loop
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class BackgroundMatcher:
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
activeOnStart: Optional[bool] = False,
|
|
189
|
+
beginsPattern: Optional[str] = None,
|
|
190
|
+
endsPattern: Optional[str] = None
|
|
191
|
+
):
|
|
192
|
+
|
|
193
|
+
self.activeOnStart = activeOnStart
|
|
194
|
+
self.beginsPattern = beginsPattern
|
|
195
|
+
self.endsPattern = endsPattern
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ProblemMatcher:
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
base: Optional[str] = None,
|
|
202
|
+
owner: Optional[str] = 'external',
|
|
203
|
+
source: Optional[str] = None,
|
|
204
|
+
severity: Optional[Literal['error', 'warning', 'info']] = 'error',
|
|
205
|
+
fileLocation: Optional[str | List[str] | List[
|
|
206
|
+
Union[
|
|
207
|
+
Literal['search'],
|
|
208
|
+
Dict[str, Optional[List[str]]]
|
|
209
|
+
]
|
|
210
|
+
]] = None,
|
|
211
|
+
pattern: Optional[str | ProblemPattern | List[ProblemPattern]] = None,
|
|
212
|
+
background: Optional[BackgroundMatcher] = None
|
|
213
|
+
):
|
|
214
|
+
|
|
215
|
+
self.base = base
|
|
216
|
+
self.owner = owner
|
|
217
|
+
self.source = source
|
|
218
|
+
self.severity = severity
|
|
219
|
+
self.fileLocation = fileLocation
|
|
220
|
+
self.pattern = pattern
|
|
221
|
+
self.background = background
|
|
222
|
+
|
|
223
|
+
# we are getting this data from json
|
|
224
|
+
# so we need to cast the classes dependencies
|
|
225
|
+
if pattern:
|
|
226
|
+
self.pattern = _cast_from_json(pattern, ProblemPattern)
|
|
227
|
+
|
|
228
|
+
if background:
|
|
229
|
+
self.background = _cast_from_json(background, BackgroundMatcher)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class RunOptions:
|
|
233
|
+
def __init__(
|
|
234
|
+
self,
|
|
235
|
+
reevaluateOnRerun: Optional[bool] = True,
|
|
236
|
+
runOn: Optional[Literal['default', 'folderOpen']] = 'default'
|
|
237
|
+
):
|
|
238
|
+
|
|
239
|
+
self.reevaluateOnRerun = reevaluateOnRerun
|
|
240
|
+
self.runOn = runOn
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class IconOptions:
|
|
244
|
+
def __init__(
|
|
245
|
+
self,
|
|
246
|
+
id: str,
|
|
247
|
+
color: Optional[str] = None
|
|
248
|
+
):
|
|
249
|
+
|
|
250
|
+
self.id = id
|
|
251
|
+
self.color = color
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class InputOptions:
|
|
255
|
+
def __init__(
|
|
256
|
+
self,
|
|
257
|
+
id: str,
|
|
258
|
+
description: str,
|
|
259
|
+
default: Optional[str] = None,
|
|
260
|
+
type: Optional[Literal['promptString', 'pickString']] = 'promptString',
|
|
261
|
+
options: Optional[List[str]] = None
|
|
262
|
+
):
|
|
263
|
+
|
|
264
|
+
self.id = id
|
|
265
|
+
self.description = description
|
|
266
|
+
self.default = default
|
|
267
|
+
self.type = type
|
|
268
|
+
self.options = options
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class TaskDescription:
|
|
272
|
+
def __init__(
|
|
273
|
+
self,
|
|
274
|
+
label: str,
|
|
275
|
+
type: Literal['shell', 'process'],
|
|
276
|
+
command: str,
|
|
277
|
+
hide: Optional[bool] = None,
|
|
278
|
+
isBackground: Optional[bool] = None,
|
|
279
|
+
args: Optional[List[str]] = None,
|
|
280
|
+
options: Optional[CommandOptions] = None,
|
|
281
|
+
group: Optional[Literal['build', 'test']] = None,
|
|
282
|
+
presentation: Optional[PresentationOptions] = None,
|
|
283
|
+
problemMatcher: Optional[str | ProblemMatcher | List[str] | List[ProblemMatcher]] = None,
|
|
284
|
+
runOptions: Optional[RunOptions] = None,
|
|
285
|
+
dependsOrder: Optional[Literal['sequence', 'parallel']] = None,
|
|
286
|
+
dependsOn: Optional[List[str]] = None,
|
|
287
|
+
icon: Optional[IconOptions] = None
|
|
288
|
+
):
|
|
289
|
+
|
|
290
|
+
self.label = label
|
|
291
|
+
self.type = type
|
|
292
|
+
self.command = command
|
|
293
|
+
self.hide: bool = (hide if hide is not None else False)
|
|
294
|
+
self.isBackground = isBackground
|
|
295
|
+
self.options = options
|
|
296
|
+
self.args = args
|
|
297
|
+
self.group = group
|
|
298
|
+
self.presentation = presentation
|
|
299
|
+
self.problemMatcher = problemMatcher
|
|
300
|
+
self.runOptions = runOptions
|
|
301
|
+
self.dependsOrder = dependsOrder
|
|
302
|
+
self.dependsOn = dependsOn
|
|
303
|
+
self.icon = icon
|
|
304
|
+
|
|
305
|
+
# we are getting this data from json
|
|
306
|
+
# so we need to cast the classes dependencies
|
|
307
|
+
if options:
|
|
308
|
+
self.options = _cast_from_json(options, CommandOptions)
|
|
309
|
+
|
|
310
|
+
if presentation:
|
|
311
|
+
self.presentation = _cast_from_json(presentation, PresentationOptions)
|
|
312
|
+
|
|
313
|
+
if runOptions:
|
|
314
|
+
self.runOptions = _cast_from_json(runOptions, RunOptions)
|
|
315
|
+
|
|
316
|
+
if icon:
|
|
317
|
+
self.icon = _cast_from_json(icon, IconOptions)
|
|
318
|
+
|
|
319
|
+
def to_dict(self):
|
|
320
|
+
return {
|
|
321
|
+
'label': self.label,
|
|
322
|
+
'type': self.type,
|
|
323
|
+
'command': self.command,
|
|
324
|
+
'isBackground': self.isBackground,
|
|
325
|
+
'args': self.args,
|
|
326
|
+
'options': self.options.__dict__ if self.options else None,
|
|
327
|
+
'group': self.group,
|
|
328
|
+
'presentation': self.presentation.__dict__ if self.presentation else None,
|
|
329
|
+
'problemMatcher': self.problemMatcher,
|
|
330
|
+
'runOptions': self.runOptions.__dict__ if self.runOptions else None,
|
|
331
|
+
'dependsOrder': self.dependsOrder,
|
|
332
|
+
'dependsOn': self.dependsOn,
|
|
333
|
+
'icon': self.icon.__dict__ if self.icon else None
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class BaseTaskConfiguration:
|
|
338
|
+
def __init__(
|
|
339
|
+
self,
|
|
340
|
+
type: str,
|
|
341
|
+
command: str,
|
|
342
|
+
isBackground: Optional[bool] = None,
|
|
343
|
+
options: Optional[CommandOptions] = None,
|
|
344
|
+
args: Optional[str] = None,
|
|
345
|
+
presentation: Optional[PresentationOptions] = None,
|
|
346
|
+
problemMatcher: Optional[str | ProblemMatcher | List[str] | List[ProblemMatcher]] = None,
|
|
347
|
+
tasks: Optional[List[TaskDescription]] = None
|
|
348
|
+
):
|
|
349
|
+
|
|
350
|
+
self.type = type
|
|
351
|
+
self.command = command
|
|
352
|
+
self.isBackground = isBackground
|
|
353
|
+
self.options = options
|
|
354
|
+
self.args = args
|
|
355
|
+
self.presentation = presentation
|
|
356
|
+
self.problemMatcher = problemMatcher
|
|
357
|
+
self.tasks = tasks
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class TaskConfiguration:
|
|
361
|
+
"""
|
|
362
|
+
TorizonConfiguration is a interface to map tasks.json file
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
def __init__(
|
|
366
|
+
self,
|
|
367
|
+
version: Literal['2.0.0'] = '2.0.0',
|
|
368
|
+
tasks: Optional[List[TaskDescription]] = None,
|
|
369
|
+
inputs: Optional[List[InputOptions]] = None,
|
|
370
|
+
windows: Optional[BaseTaskConfiguration] = None,
|
|
371
|
+
osx: Optional[BaseTaskConfiguration] = None,
|
|
372
|
+
linux: Optional[BaseTaskConfiguration] = None
|
|
373
|
+
):
|
|
374
|
+
|
|
375
|
+
self.version = version
|
|
376
|
+
self.tasks = tasks
|
|
377
|
+
self.inputs = inputs
|
|
378
|
+
self.windows = windows
|
|
379
|
+
self.osx = osx
|
|
380
|
+
self.linux = linux
|
|
381
|
+
|
|
382
|
+
# as this could be from json dict, we need to cast the tasks
|
|
383
|
+
if tasks:
|
|
384
|
+
self.tasks = [_cast_from_json(task, TaskDescription) for task in tasks]
|
|
385
|
+
|
|
386
|
+
if inputs:
|
|
387
|
+
self.inputs = [_cast_from_json(_input, InputOptions) for _input in inputs]
|
|
388
|
+
|
|
389
|
+
# TODO:
|
|
390
|
+
# for now we are not casting the other configurations
|
|
391
|
+
# as them are not used in the templates
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def get_tasks_json(file_path: str) -> TaskConfiguration:
|
|
395
|
+
with open(f"{file_path}/.vscode/tasks.json", 'r') as file:
|
|
396
|
+
return _cast_from_json(json.load(file), TaskConfiguration)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def get_settings_json(
|
|
400
|
+
file_path: str,
|
|
401
|
+
custom_file: str | None = None
|
|
402
|
+
) -> TorizonSettings:
|
|
403
|
+
_file = custom_file if custom_file else "settings.json"
|
|
404
|
+
|
|
405
|
+
with open(f"{file_path}/.vscode/{_file}", 'r') as file:
|
|
406
|
+
_tor_settings = _cast_from_json(json.load(file), TorizonSettings)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
return _tor_settings
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class TaskRunner:
|
|
414
|
+
"""
|
|
415
|
+
TaskRunner is a class to run tasks from tasks.json file
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
def __init__(
|
|
419
|
+
self,
|
|
420
|
+
tasks: List[TaskDescription],
|
|
421
|
+
inputs: List[InputOptions],
|
|
422
|
+
settings: TorizonSettings,
|
|
423
|
+
debug: bool = False
|
|
424
|
+
):
|
|
425
|
+
|
|
426
|
+
self.__tasks = tasks
|
|
427
|
+
self.__inputs = inputs
|
|
428
|
+
self.__settings = settings
|
|
429
|
+
self.__debug = debug
|
|
430
|
+
self.__gitlab_ci = False
|
|
431
|
+
self.__tasks_override_env = False
|
|
432
|
+
self.__cli_inputs: Dict[str, str] = {}
|
|
433
|
+
self.__can_receive_interactive_input = False
|
|
434
|
+
|
|
435
|
+
# check if we have stdin
|
|
436
|
+
if os.isatty(0) and (("TASKS_DISABLE_INTERACTIVE_INPUT" not in os.environ) or (os.environ["TASKS_DISABLE_INTERACTIVE_INPUT"] != "True")):
|
|
437
|
+
self.__can_receive_interactive_input = True
|
|
438
|
+
|
|
439
|
+
# environment configs
|
|
440
|
+
if "DOCKER_PSSWD" in os.environ:
|
|
441
|
+
os.environ["config:docker_password"] = os.environ["DOCKER_PSSWD"]
|
|
442
|
+
|
|
443
|
+
if "GITLAB_CI" in os.environ:
|
|
444
|
+
self.__gitlab_ci = True
|
|
445
|
+
|
|
446
|
+
if "TASKS_OVERRIDE_ENV" in os.environ and os.environ["TASKS_OVERRIDE_ENV"] == "True":
|
|
447
|
+
self.__tasks_override_env = True
|
|
448
|
+
|
|
449
|
+
if "TASKS_DEBUG" in os.environ and os.environ["TASKS_DEBUG"] == "True":
|
|
450
|
+
self.__debug = True
|
|
451
|
+
|
|
452
|
+
self.__settings_to_env()
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def __settings_to_env(self):
|
|
456
|
+
# for keys in settings, we are adding to env
|
|
457
|
+
for key, value in self.__settings.__dict__.items():
|
|
458
|
+
if value is not None:
|
|
459
|
+
os.environ[f"config:{key}"] = f"{value}"
|
|
460
|
+
|
|
461
|
+
# also for non Torizon ones
|
|
462
|
+
for key, value in self.__settings.any.items():
|
|
463
|
+
if isinstance(value, str) or \
|
|
464
|
+
isinstance(value, int) or \
|
|
465
|
+
isinstance(value, float):
|
|
466
|
+
|
|
467
|
+
os.environ[f"config:{key}"] = str(value)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def list_labels(self, show_hidden=False, no_index: bool = False):
|
|
471
|
+
i = 0
|
|
472
|
+
|
|
473
|
+
for task in self.__tasks:
|
|
474
|
+
if no_index:
|
|
475
|
+
if show_hidden or not task.hide:
|
|
476
|
+
print(task.label, flush=True)
|
|
477
|
+
else:
|
|
478
|
+
if show_hidden or not task.hide:
|
|
479
|
+
print(f"{i}. \t{task.label}", flush=True)
|
|
480
|
+
|
|
481
|
+
i += 1
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def desc_input(self, id: str):
|
|
485
|
+
for _input in self.__inputs:
|
|
486
|
+
if _input.id == id:
|
|
487
|
+
print(json.dumps(_input.__dict__, indent=4))
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
raise ReferenceError(f"Input with id [{id}] not found")
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def desc_task(self, label: int | str):
|
|
494
|
+
task = None
|
|
495
|
+
|
|
496
|
+
if isinstance(label, int):
|
|
497
|
+
task = self.__tasks[label]
|
|
498
|
+
else:
|
|
499
|
+
for _task in self.__tasks:
|
|
500
|
+
if _task.label == label:
|
|
501
|
+
task = _task
|
|
502
|
+
break
|
|
503
|
+
|
|
504
|
+
if task is not None:
|
|
505
|
+
task_txt = json.dumps(task.to_dict(), indent=4)
|
|
506
|
+
print(task_txt, flush=True)
|
|
507
|
+
else:
|
|
508
|
+
raise ReferenceError(f"Task with index [{label}] not found")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def __replace_env_var(self, var: str, env: str):
|
|
512
|
+
if f"${{{var}}}" in env:
|
|
513
|
+
return env.replace(f"${{{var}}}", os.environ[var])
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def __check_workspace_folder(self, env: List[str]) -> List[str]:
|
|
517
|
+
ret: List[str] = []
|
|
518
|
+
|
|
519
|
+
for value in env:
|
|
520
|
+
if "workspaceFolderBasename" in value:
|
|
521
|
+
value = self.__replace_env_var("workspaceFolderBasename", value)
|
|
522
|
+
if "workspaceFolder" in value:
|
|
523
|
+
value = self.__replace_env_var("workspaceFolder", value)
|
|
524
|
+
ret.append(value)
|
|
525
|
+
|
|
526
|
+
return ret
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def __check_torizon_inputs(self, env: List[str]) -> List[str]:
|
|
530
|
+
ret: List[str] = []
|
|
531
|
+
|
|
532
|
+
for value in env:
|
|
533
|
+
if "${command:torizon_" in value:
|
|
534
|
+
value = value.replace("${command:torizon_", "${config:torizon_")
|
|
535
|
+
ret.append(value)
|
|
536
|
+
|
|
537
|
+
return ret
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def __check_docker_inputs(self, env: List[str]) -> List[str]:
|
|
541
|
+
ret: List[str] = []
|
|
542
|
+
|
|
543
|
+
for value in env:
|
|
544
|
+
if "${command:docker_" in value:
|
|
545
|
+
value = value.replace("${command:docker_", "${config:docker_")
|
|
546
|
+
ret.append(value)
|
|
547
|
+
|
|
548
|
+
return ret
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def __check_tcb_inputs(self, env: List[str]) -> List[str]:
|
|
552
|
+
ret: List[str] = []
|
|
553
|
+
|
|
554
|
+
for value in env:
|
|
555
|
+
if "${command:tcb" in value:
|
|
556
|
+
if "tcb.getNextPackageVersion" in value:
|
|
557
|
+
# call the xonsh script
|
|
558
|
+
_p_ret = subprocess.run(
|
|
559
|
+
[
|
|
560
|
+
"xonsh",
|
|
561
|
+
"./.conf/torizon-io.xsh",
|
|
562
|
+
"package", "latest", "version",
|
|
563
|
+
os.environ["config:tcb_packageName"]
|
|
564
|
+
],
|
|
565
|
+
capture_output=True,
|
|
566
|
+
text=True,
|
|
567
|
+
env=os.environ
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
if _p_ret.returncode != 0:
|
|
571
|
+
# Sometimes the error is presented on stdout and not stderr
|
|
572
|
+
raise RuntimeError(f"Error running torizon-io.xsh: {_p_ret}")
|
|
573
|
+
|
|
574
|
+
# TODO: Maybe not use this and instead disable colors on the terminal
|
|
575
|
+
# Remove ANSI escape sequences. Regex from this thread:
|
|
576
|
+
# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
|
|
577
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
578
|
+
_latest_ver = ansi_escape.sub('', _p_ret.stdout.strip())
|
|
579
|
+
|
|
580
|
+
_latest_ver_number = _latest_ver.rsplit('-', 1)[-1]
|
|
581
|
+
_latest_ver_last_number = _latest_ver_number.rsplit('.', 1)[-1]
|
|
582
|
+
try:
|
|
583
|
+
_next = int(_latest_ver_last_number) +1
|
|
584
|
+
except:
|
|
585
|
+
raise ValueError(f"Package version should be in one of the following formats: <int>, <string-int>, <major.minor.patch>, or <string-major.minor.patch>. Depending on format, int or patch value will be incremented")
|
|
586
|
+
|
|
587
|
+
if self.__debug:
|
|
588
|
+
print(f"Next package version: {_next}", flush=True)
|
|
589
|
+
|
|
590
|
+
value = value.replace(f"${{command:tcb.getNextPackageVersion}}", f"{_next}")
|
|
591
|
+
|
|
592
|
+
elif "tcb.outputTEZIFolder" in value:
|
|
593
|
+
# load the tcbuild.yaml
|
|
594
|
+
with open("tcbuild.yaml", 'r') as file:
|
|
595
|
+
_tcbuild = yaml.load(file, Loader=yaml.FullLoader)
|
|
596
|
+
|
|
597
|
+
_tezi_folder = None
|
|
598
|
+
try:
|
|
599
|
+
_tezi_folder = _tcbuild["output"]["easy-installer"]["local"]
|
|
600
|
+
except KeyError:
|
|
601
|
+
raise RuntimeError("Error replacing variable tcb.outputTEZIFolder, make sure the tcbuild.yaml has the output.easy-installer.local property")
|
|
602
|
+
|
|
603
|
+
value = value.replace(f"${{command:tcb.outputTEZIFolder}}", _tezi_folder)
|
|
604
|
+
|
|
605
|
+
# for all the items we need to replace ${command:tcb. with ${config:tcb.
|
|
606
|
+
_pattern = r"(?<=\$\{command:tcb\.).*?(?=\s*})"
|
|
607
|
+
_matches = re.findall(_pattern, value)
|
|
608
|
+
|
|
609
|
+
for match in _matches:
|
|
610
|
+
value = value.replace(f"${{command:tcb.{match}}}", f"${{config:tcb.{match}}}")
|
|
611
|
+
|
|
612
|
+
ret.append(value)
|
|
613
|
+
|
|
614
|
+
return ret
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def __contains_special_chars(self, str: str) -> bool:
|
|
618
|
+
_pattern = r"[^a-zA-Z0-9\.\-_|>\/=+&_]"
|
|
619
|
+
return re.search(_pattern, str) is not None
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def __scape_args(self, args: List[str]) -> List[str]:
|
|
623
|
+
ret: List[str] = []
|
|
624
|
+
|
|
625
|
+
for arg in args:
|
|
626
|
+
if "\"" in arg:
|
|
627
|
+
arg = arg.replace("\"", "\\\"")
|
|
628
|
+
|
|
629
|
+
ret.append(arg)
|
|
630
|
+
|
|
631
|
+
return ret
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def __check_config(self, args: List[str]) -> List[str]:
|
|
635
|
+
"""
|
|
636
|
+
This method will make the config replacement in the args
|
|
637
|
+
"""
|
|
638
|
+
ret: List[str] = []
|
|
639
|
+
|
|
640
|
+
for arg in args:
|
|
641
|
+
if "${config:" in arg:
|
|
642
|
+
_pattern = r"(?<=\$\{config:).*?(?=\s*})"
|
|
643
|
+
_matches = re.findall(_pattern, arg)
|
|
644
|
+
|
|
645
|
+
for match in _matches:
|
|
646
|
+
if "." in match:
|
|
647
|
+
_match = match.replace(".", "_")
|
|
648
|
+
else:
|
|
649
|
+
_match = match
|
|
650
|
+
|
|
651
|
+
# first check if the config exists
|
|
652
|
+
if f"config:{_match}" not in os.environ:
|
|
653
|
+
raise ReferenceError(f"Config with id [{match}] not found. Check your settings.json")
|
|
654
|
+
|
|
655
|
+
# edge case for docker_registry
|
|
656
|
+
if _match == "docker_registry" and os.environ[f"config:{_match}"] == "":
|
|
657
|
+
os.environ[f"config:{_match}"] = "registry-1.docker.io"
|
|
658
|
+
|
|
659
|
+
arg = arg.replace(f"${{config:{match}}}", os.environ[f"config:{_match}"])
|
|
660
|
+
|
|
661
|
+
ret.append(arg)
|
|
662
|
+
|
|
663
|
+
return ret
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def __check_vscode_env(self, args: List[str]) -> List[str]:
|
|
667
|
+
"""
|
|
668
|
+
handle the VS Code ${env:VAR} replacement
|
|
669
|
+
"""
|
|
670
|
+
ret: List[str] = []
|
|
671
|
+
|
|
672
|
+
for arg in args:
|
|
673
|
+
if "${env:" in arg:
|
|
674
|
+
_pattern = r"(?<=\$\{env:).*?(?=\s*})"
|
|
675
|
+
_matches = re.findall(_pattern, arg)
|
|
676
|
+
|
|
677
|
+
for match in _matches:
|
|
678
|
+
if match not in os.environ:
|
|
679
|
+
raise ReferenceError(f"Environment variable with id [{match}] not found")
|
|
680
|
+
|
|
681
|
+
arg = arg.replace(f"${{env:{match}}}", os.environ[match])
|
|
682
|
+
|
|
683
|
+
ret.append(arg)
|
|
684
|
+
|
|
685
|
+
return ret
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def __check_long_args(self, args: List[str]) -> List[str]:
|
|
689
|
+
ret: List[str] = []
|
|
690
|
+
|
|
691
|
+
for arg in args:
|
|
692
|
+
if " " in arg:
|
|
693
|
+
arg = f"'{arg}'"
|
|
694
|
+
|
|
695
|
+
ret.append(arg)
|
|
696
|
+
|
|
697
|
+
return ret
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def __quoting_special_chars(self, args: List[str]) -> List[str]:
|
|
701
|
+
ret: List[str] = []
|
|
702
|
+
|
|
703
|
+
for arg in args:
|
|
704
|
+
_has_special_chars = self.__contains_special_chars(arg)
|
|
705
|
+
_hash_space = " " in arg
|
|
706
|
+
|
|
707
|
+
if _has_special_chars and not _hash_space:
|
|
708
|
+
arg = f"'{arg}'"
|
|
709
|
+
|
|
710
|
+
ret.append(arg)
|
|
711
|
+
|
|
712
|
+
return ret
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def __check_input(self, args: List[str]) -> List[str]:
|
|
716
|
+
ret: List[str] = []
|
|
717
|
+
|
|
718
|
+
for arg in args:
|
|
719
|
+
if "${input:" in arg:
|
|
720
|
+
_pattern = r"(?<=\$\{input:).*?(?=\s*})"
|
|
721
|
+
_matches = re.findall(_pattern, arg)
|
|
722
|
+
|
|
723
|
+
for match in _matches:
|
|
724
|
+
_input = None
|
|
725
|
+
_input_value = "None"
|
|
726
|
+
|
|
727
|
+
for inp in self.__inputs:
|
|
728
|
+
if inp.id == match:
|
|
729
|
+
_input = inp
|
|
730
|
+
break
|
|
731
|
+
|
|
732
|
+
if _input is None:
|
|
733
|
+
raise ReferenceError(f"Input with id [{match}] not found")
|
|
734
|
+
|
|
735
|
+
# first check if the input was set by cli
|
|
736
|
+
if match in self.__cli_inputs:
|
|
737
|
+
_input_value = self.__cli_inputs[match]
|
|
738
|
+
elif _input.default:
|
|
739
|
+
_input_value = _input.default
|
|
740
|
+
else:
|
|
741
|
+
if not self.__can_receive_interactive_input:
|
|
742
|
+
raise RuntimeError("CLI inputs not set and interactive input is disabled")
|
|
743
|
+
|
|
744
|
+
if _input.type == "promptString":
|
|
745
|
+
_input_value = input(f"{_input.description}: ")
|
|
746
|
+
elif _input.type == "pickString":
|
|
747
|
+
for _inp in self.__inputs:
|
|
748
|
+
if _inp.id == match:
|
|
749
|
+
# print options
|
|
750
|
+
assert _inp.options is not None, "pickString option has a valid id but options is empty. Check your tasks.json"
|
|
751
|
+
print(f"Options for [{match}]:")
|
|
752
|
+
_i = 0
|
|
753
|
+
_indexed_options = {}
|
|
754
|
+
|
|
755
|
+
for _opt in _inp.options:
|
|
756
|
+
_indexed_options[str(_i)] = _opt
|
|
757
|
+
print(f"{_i}. {_opt}")
|
|
758
|
+
_i += 1
|
|
759
|
+
|
|
760
|
+
_input_value = input(f"{_input.description} (option index): ")
|
|
761
|
+
|
|
762
|
+
# check if the input is in the options
|
|
763
|
+
if _input_value not in _indexed_options:
|
|
764
|
+
raise ValueError(f"Input value for [{match}] is not in the possible options")
|
|
765
|
+
else:
|
|
766
|
+
_input_value = _indexed_options[_input_value]
|
|
767
|
+
|
|
768
|
+
if _input_value is None:
|
|
769
|
+
raise ValueError(f"Input value for [{match}] could not be None")
|
|
770
|
+
|
|
771
|
+
arg = arg.replace(f"${{input:{match}}}", _input_value)
|
|
772
|
+
|
|
773
|
+
ret.append(arg)
|
|
774
|
+
|
|
775
|
+
return ret
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def __parse_envs(self, env: str, task: TaskDescription) -> str | None :
|
|
779
|
+
"""
|
|
780
|
+
It's christmas time 🎅
|
|
781
|
+
"""
|
|
782
|
+
if task.options:
|
|
783
|
+
value = task.options.env
|
|
784
|
+
|
|
785
|
+
# get the env from the task
|
|
786
|
+
if value:
|
|
787
|
+
_env_value = value.get(env)
|
|
788
|
+
|
|
789
|
+
if _env_value:
|
|
790
|
+
expvalue = [_env_value]
|
|
791
|
+
expvalue = self.__check_workspace_folder(expvalue)
|
|
792
|
+
expvalue = self.__check_torizon_inputs(expvalue)
|
|
793
|
+
expvalue = self.__check_docker_inputs(expvalue)
|
|
794
|
+
expvalue = self.__check_tcb_inputs(expvalue)
|
|
795
|
+
expvalue = self.__check_input(expvalue)
|
|
796
|
+
expvalue = self.__check_config(expvalue)
|
|
797
|
+
exp_value_str = " ".join(expvalue)
|
|
798
|
+
|
|
799
|
+
if self.__debug:
|
|
800
|
+
print(f"Env: {env}={_env_value}", color=Color.YELLOW, flush=True)
|
|
801
|
+
print(f"Parsed Env: {env}={exp_value_str}", color=Color.YELLOW, flush=True)
|
|
802
|
+
|
|
803
|
+
return exp_value_str
|
|
804
|
+
|
|
805
|
+
return None
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def __replace_docker_host(self, arg: str) -> str:
|
|
809
|
+
if "DOCKER_HOST" in arg:
|
|
810
|
+
arg = arg.replace("DOCKER_HOST=", "DOCKER_HOST=tcp://docker:2375")
|
|
811
|
+
|
|
812
|
+
return arg
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def set_cli_inputs(self, cli_inputs: Dict[str, str]) -> None:
|
|
816
|
+
"""
|
|
817
|
+
Set the cli inputs to be used in the tasks.
|
|
818
|
+
"""
|
|
819
|
+
for key, value in cli_inputs.items():
|
|
820
|
+
# validate if the key is in the inputs
|
|
821
|
+
_input = None
|
|
822
|
+
_input = next((inp for inp in self.__inputs if inp.id == key), None)
|
|
823
|
+
|
|
824
|
+
if _input is None:
|
|
825
|
+
raise ReferenceError(f"Input with id [{key}] not found")
|
|
826
|
+
|
|
827
|
+
self.__cli_inputs[key] = value
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def run_task(self, label: str) -> None:
|
|
831
|
+
# query the task
|
|
832
|
+
_task = None
|
|
833
|
+
_task = next((task for task in self.__tasks if task.label == label), None)
|
|
834
|
+
|
|
835
|
+
if _task is None:
|
|
836
|
+
raise ReferenceError(f"Task with label [{label}] not found")
|
|
837
|
+
|
|
838
|
+
_depends = []
|
|
839
|
+
if _task.dependsOn is not None:
|
|
840
|
+
_depends = _task.dependsOn
|
|
841
|
+
|
|
842
|
+
# first we need to run the dependencies
|
|
843
|
+
for dep in _depends:
|
|
844
|
+
self.run_task(dep)
|
|
845
|
+
|
|
846
|
+
print(f"> Executing task: {label} <", color=Color.GREEN, flush=True)
|
|
847
|
+
|
|
848
|
+
# prepare the command
|
|
849
|
+
_cmd = _task.command
|
|
850
|
+
|
|
851
|
+
# the cmd itself can use the mechanism to replace stuff
|
|
852
|
+
_cmd = self.__check_workspace_folder([_cmd])[0]
|
|
853
|
+
_cmd = self.__check_torizon_inputs([_cmd])[0]
|
|
854
|
+
_cmd = self.__check_docker_inputs([_cmd])[0]
|
|
855
|
+
_cmd = self.__check_tcb_inputs([_cmd])[0]
|
|
856
|
+
_cmd = self.__check_input([_cmd])[0]
|
|
857
|
+
_cmd = self.__check_vscode_env([_cmd])[0]
|
|
858
|
+
_cmd = self.__check_config([_cmd])[0]
|
|
859
|
+
|
|
860
|
+
_args = []
|
|
861
|
+
if _task.args is not None:
|
|
862
|
+
_args = _task.args
|
|
863
|
+
|
|
864
|
+
_env: Dict[str, str] | None = {}
|
|
865
|
+
_cwd = None
|
|
866
|
+
_last_cwd = os.getcwd()
|
|
867
|
+
if _task.options is not None:
|
|
868
|
+
_env = _task.options.env
|
|
869
|
+
_cwd = _task.options.cwd
|
|
870
|
+
|
|
871
|
+
_is_background = ""
|
|
872
|
+
if _task.isBackground:
|
|
873
|
+
_is_background = " &"
|
|
874
|
+
|
|
875
|
+
_shell = _task.type == "shell"
|
|
876
|
+
|
|
877
|
+
# FIXME: The scape args was in the powershell implementation
|
|
878
|
+
# but when used on Python it generates weird behavior
|
|
879
|
+
# _args = self.__scape_args(_args)
|
|
880
|
+
_args = self.__check_workspace_folder(_args)
|
|
881
|
+
_args = self.__check_torizon_inputs(_args)
|
|
882
|
+
_args = self.__check_docker_inputs(_args)
|
|
883
|
+
_args = self.__check_tcb_inputs(_args)
|
|
884
|
+
_args = self.__check_input(_args)
|
|
885
|
+
_args = self.__check_vscode_env(_args)
|
|
886
|
+
_args = self.__check_config(_args)
|
|
887
|
+
_args = self.__check_workspace_folder(_args)
|
|
888
|
+
# FIXME: These was in the powershell implementation
|
|
889
|
+
# but when used on Python it generates weird behavior
|
|
890
|
+
# _args = self.__check_long_args(_args)
|
|
891
|
+
# _args = self.__quoting_special_chars(_args)
|
|
892
|
+
|
|
893
|
+
# if in gitlab ci env we need to replace the DOCKER_HOST
|
|
894
|
+
if self.__gitlab_ci:
|
|
895
|
+
_cmd = self.__replace_docker_host(_cmd)
|
|
896
|
+
|
|
897
|
+
task_env = os.environ.copy()
|
|
898
|
+
# If TASKS_OVERRIDE_ENV is true, override the env vars with the
|
|
899
|
+
# env var values present on the task. If false, set just the env vars
|
|
900
|
+
# present on the task that doesn't already exist on the env var
|
|
901
|
+
if _env is not None:
|
|
902
|
+
for env, _ in _env.items():
|
|
903
|
+
if self.__tasks_override_env == True or env not in os.environ:
|
|
904
|
+
__parsed_env_value = self.__parse_envs(env, _task)
|
|
905
|
+
task_env[env] = __parsed_env_value
|
|
906
|
+
|
|
907
|
+
# we need to change the cwd if it's set
|
|
908
|
+
if _cwd is not None:
|
|
909
|
+
_cwd = self.__check_workspace_folder([_cwd])[0]
|
|
910
|
+
_cwd = self.__check_config([_cwd])[0]
|
|
911
|
+
_cwd = self.__check_vscode_env([_cwd])[0]
|
|
912
|
+
|
|
913
|
+
os.chdir(_cwd)
|
|
914
|
+
|
|
915
|
+
# execute the task
|
|
916
|
+
_cmd_join = f"{_cmd} {' '.join(_args)}{_is_background}"
|
|
917
|
+
|
|
918
|
+
if self.__debug:
|
|
919
|
+
print(f"Command: {_task.command}", color=Color.YELLOW, flush=True)
|
|
920
|
+
print(f"Args: {_task.args}", color=Color.YELLOW, flush=True)
|
|
921
|
+
print(f"Parsed Args: {_args}", color=Color.YELLOW, flush=True)
|
|
922
|
+
print(f"Parsed Command: {_cmd_join}", color=Color.YELLOW, flush=True)
|
|
923
|
+
|
|
924
|
+
# use bash to execute the VSCode tasks commands and scripts, as they
|
|
925
|
+
# are written and tested in bash. Valid just for commands of shell
|
|
926
|
+
# type, not process type ones
|
|
927
|
+
_ret = subprocess.run(
|
|
928
|
+
[_cmd, *_args] if not _shell else _cmd_join,
|
|
929
|
+
stdout=None,
|
|
930
|
+
stderr=None,
|
|
931
|
+
env=task_env,
|
|
932
|
+
shell=_shell,
|
|
933
|
+
executable="/bin/bash" if _shell else None
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
# go back to the last cwd
|
|
937
|
+
os.chdir(_last_cwd)
|
|
938
|
+
|
|
939
|
+
if _ret.returncode != 0:
|
|
940
|
+
print(f"> TASK [{label}] exited with error code [{_ret.returncode}] <", color=Color.RED, flush=True)
|
|
941
|
+
raise RuntimeError(f"Error running task: {label}")
|