dev-tools-loader 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KARMA-Electronics
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: dev_tools_loader
3
+ Version: 0.1.0
4
+ Summary: Development tools loader
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://gitlab.com/karma_electronics/desktop/dev_tools_loader
7
+ Project-URL: Repository, https://gitlab.com/karma_electronics/desktop/dev_tools_loader.git
8
+ Requires-Python: >=3.6
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # Development Tools Loader
14
+
15
+ A CLI tool for automated downloading of development tools:
16
+
17
+ - VS Code installers and extensions (`.vsix` files).
18
+ - Python installers and pip packages;
19
+
20
+ The tool ensures version compatibility, handles dependencies, and supports repeatable configurations via JSON.
21
+
22
+
23
+ ## Key Features
24
+
25
+ - **Version control**: Ensures VS Code extension versions match the target VS Code engine version.
26
+ - **Package bundles**: Supports downloading multiple packages/extensions in a single config.
27
+ - **Dependency handling**: Automatically resolves and downloads dependencies for VS Code extensions.
28
+ - **Resilient downloads**: Automatically retries on connection loss.
29
+ - **Repeatable setups**: JSON-based configuration enables reproducible environment setups.
30
+ - **Flexible versioning**: Supports `"latest"` for extensions and packages.
31
+
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install dev-tools-loader
37
+ ```
38
+
39
+
40
+ ## Running
41
+
42
+ Once installed, you can run the tool from the command line using the JSON configuration file.
43
+
44
+ ```bash
45
+ dev_tools_loader -j path/to/config.json
46
+ ```
47
+
48
+
49
+ ## Command‑Line Options
50
+
51
+ - **`-j`, `--json-path` *`<json_config_path>`*** **(required)** Specifies the path to the JSON configuration file that defines download targets.
52
+ - **`-o`, `--output-path` *`<output_dir>`*** Sets the output directory where downloaded files will be saved.
53
+ - **`-c`, `--clean`** If specified, deletes files in the target output directory before starting the download process.
54
+ - **`-h`, `--help`** Displays the help message with a summary of all available options and exits.
55
+ - **`--version`** Prints the current version of the `dev-tools-loader` package and exits.
56
+
57
+
58
+ ## Example Config
59
+
60
+ ```json
61
+ {
62
+ "version": "0.1.0",
63
+ "targets": [
64
+ {
65
+ "type": "python",
66
+ "platform": "win_amd64",
67
+ "version": "3.12.0",
68
+ "installer": "load",
69
+ "packages": [
70
+ {
71
+ "name": "compiledb",
72
+ "version": "0.10.6"
73
+ },
74
+ {
75
+ "name": "requests",
76
+ "version": "latest"
77
+ }
78
+ ]
79
+ },
80
+ {
81
+ "type": "vscode",
82
+ "platform": "win32-x64",
83
+ "version": "1.96.0",
84
+ "installer": "load",
85
+ "extensions": [
86
+ {
87
+ "uid": "ms-vscode.cpptools",
88
+ "version": "1.28.0"
89
+ },
90
+ {
91
+ "uid": "ms-python.python",
92
+ "version": "latest"
93
+ }
94
+ ]
95
+ }
96
+ ]
97
+ }
98
+ ```
99
+
100
+
101
+ ## Configuration Fields
102
+
103
+ - `version` (str): Schema version.
104
+ - `targets` (list): List of download targets. Each target has:
105
+ - `type` (str): `"python"` or `"vscode"`.
106
+ - `platform` (str): Target platform (see supported platforms below).
107
+ - `version` (str): Version of the tool.
108
+ - `installer` (str): `"load"` to download installer or `"skip"`.
109
+ - `packages` (list, Python-only): List of pip packages to download.
110
+ - `name` (str): Package name.
111
+ - `version` (str): Package version (`"latest"` supported).
112
+ - `extensions` (list, VS Code-only): List of VS Code extensions to download.
113
+ - `uid` (str): Extension ID (e.g., `"ms-vscode.cpptools"`).
114
+ - `version` (str): Extension version (`"latest"` supported).
115
+
116
+
117
+ ## Supported Platforms Python
118
+
119
+ - `win32`
120
+ - `win_amd64`
121
+ - `win_arm64`
122
+ - `manylinux1_x86_64`
123
+ - `manylinux2010_x86_64`
124
+ - `manylinux2014_x86_64`
125
+ - `manylinux1_i686`
126
+ - `manylinux2010_i686`
127
+ - `manylinux2014_i686`
128
+ - `manylinux2014_aarch64`
129
+ - `manylinux2014_armv7l`
130
+ - `macosx_10_9_x86_64`
131
+ - `macosx_11_0_arm64`
132
+
133
+ ## Supported Platforms VS Code
134
+
135
+ - `win32-x64`
136
+ - `win32-arm64`
137
+ - `linux-x64`
138
+ - `linux-arm64`
139
+ - `linux-armhf`
140
+ - `alpine-x64`
141
+ - `alpine-arm64`
142
+ - `darwin-x64`
143
+ - `darwin-arm64`
@@ -0,0 +1,131 @@
1
+ # Development Tools Loader
2
+
3
+ A CLI tool for automated downloading of development tools:
4
+
5
+ - VS Code installers and extensions (`.vsix` files).
6
+ - Python installers and pip packages;
7
+
8
+ The tool ensures version compatibility, handles dependencies, and supports repeatable configurations via JSON.
9
+
10
+
11
+ ## Key Features
12
+
13
+ - **Version control**: Ensures VS Code extension versions match the target VS Code engine version.
14
+ - **Package bundles**: Supports downloading multiple packages/extensions in a single config.
15
+ - **Dependency handling**: Automatically resolves and downloads dependencies for VS Code extensions.
16
+ - **Resilient downloads**: Automatically retries on connection loss.
17
+ - **Repeatable setups**: JSON-based configuration enables reproducible environment setups.
18
+ - **Flexible versioning**: Supports `"latest"` for extensions and packages.
19
+
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install dev-tools-loader
25
+ ```
26
+
27
+
28
+ ## Running
29
+
30
+ Once installed, you can run the tool from the command line using the JSON configuration file.
31
+
32
+ ```bash
33
+ dev_tools_loader -j path/to/config.json
34
+ ```
35
+
36
+
37
+ ## Command‑Line Options
38
+
39
+ - **`-j`, `--json-path` *`<json_config_path>`*** **(required)** Specifies the path to the JSON configuration file that defines download targets.
40
+ - **`-o`, `--output-path` *`<output_dir>`*** Sets the output directory where downloaded files will be saved.
41
+ - **`-c`, `--clean`** If specified, deletes files in the target output directory before starting the download process.
42
+ - **`-h`, `--help`** Displays the help message with a summary of all available options and exits.
43
+ - **`--version`** Prints the current version of the `dev-tools-loader` package and exits.
44
+
45
+
46
+ ## Example Config
47
+
48
+ ```json
49
+ {
50
+ "version": "0.1.0",
51
+ "targets": [
52
+ {
53
+ "type": "python",
54
+ "platform": "win_amd64",
55
+ "version": "3.12.0",
56
+ "installer": "load",
57
+ "packages": [
58
+ {
59
+ "name": "compiledb",
60
+ "version": "0.10.6"
61
+ },
62
+ {
63
+ "name": "requests",
64
+ "version": "latest"
65
+ }
66
+ ]
67
+ },
68
+ {
69
+ "type": "vscode",
70
+ "platform": "win32-x64",
71
+ "version": "1.96.0",
72
+ "installer": "load",
73
+ "extensions": [
74
+ {
75
+ "uid": "ms-vscode.cpptools",
76
+ "version": "1.28.0"
77
+ },
78
+ {
79
+ "uid": "ms-python.python",
80
+ "version": "latest"
81
+ }
82
+ ]
83
+ }
84
+ ]
85
+ }
86
+ ```
87
+
88
+
89
+ ## Configuration Fields
90
+
91
+ - `version` (str): Schema version.
92
+ - `targets` (list): List of download targets. Each target has:
93
+ - `type` (str): `"python"` or `"vscode"`.
94
+ - `platform` (str): Target platform (see supported platforms below).
95
+ - `version` (str): Version of the tool.
96
+ - `installer` (str): `"load"` to download installer or `"skip"`.
97
+ - `packages` (list, Python-only): List of pip packages to download.
98
+ - `name` (str): Package name.
99
+ - `version` (str): Package version (`"latest"` supported).
100
+ - `extensions` (list, VS Code-only): List of VS Code extensions to download.
101
+ - `uid` (str): Extension ID (e.g., `"ms-vscode.cpptools"`).
102
+ - `version` (str): Extension version (`"latest"` supported).
103
+
104
+
105
+ ## Supported Platforms Python
106
+
107
+ - `win32`
108
+ - `win_amd64`
109
+ - `win_arm64`
110
+ - `manylinux1_x86_64`
111
+ - `manylinux2010_x86_64`
112
+ - `manylinux2014_x86_64`
113
+ - `manylinux1_i686`
114
+ - `manylinux2010_i686`
115
+ - `manylinux2014_i686`
116
+ - `manylinux2014_aarch64`
117
+ - `manylinux2014_armv7l`
118
+ - `macosx_10_9_x86_64`
119
+ - `macosx_11_0_arm64`
120
+
121
+ ## Supported Platforms VS Code
122
+
123
+ - `win32-x64`
124
+ - `win32-arm64`
125
+ - `linux-x64`
126
+ - `linux-arm64`
127
+ - `linux-armhf`
128
+ - `alpine-x64`
129
+ - `alpine-arm64`
130
+ - `darwin-x64`
131
+ - `darwin-arm64`
@@ -0,0 +1,5 @@
1
+ from .dev_tools_loader import DevToolsLoader
2
+
3
+ __all__ = ['DevToolsLoader']
4
+
5
+ __version__ = '0.1.0'
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == '__main__':
4
+ main()
@@ -0,0 +1,31 @@
1
+ import os
2
+ import argparse
3
+ from pathlib import Path
4
+
5
+ from .dev_tools_loader import DebugLog, DevToolsLoader
6
+ from dev_tools_loader import __version__
7
+
8
+
9
+ def main():
10
+ parser = argparse.ArgumentParser(description='Development tools loader CLI')
11
+ parser.add_argument('-j', '--json-path', help='Path to JSON config file', action='store', required=True)
12
+ parser.add_argument('-o', '--output-path', help='Path to output dir (default ./output)', action='store', default=Path(os.getcwd()) / Path('output'))
13
+ parser.add_argument('-c', '--clean', help='Clean output before load (default False)', action='store_true')
14
+ parser.add_argument('--version', help='Show version', action='version', version=f'%(prog)s {__version__}')
15
+ args = parser.parse_args()
16
+
17
+ try:
18
+ dtl = DevToolsLoader(args.json_path, args.output_path, args.clean)
19
+ dtl.run()
20
+
21
+ except KeyboardInterrupt:
22
+ DebugLog.log(f'\n>>> Exit by user', color='yellow')
23
+
24
+ except ValueError as e:
25
+ DebugLog.log(f'\n>>> Value error: {e}', color='red')
26
+
27
+ except RuntimeError as e:
28
+ DebugLog.log(f'\n>>> Runtime error: {e}', color='red')
29
+
30
+ finally:
31
+ pass
@@ -0,0 +1,397 @@
1
+ import os
2
+ import sys
3
+ import re
4
+ import shutil
5
+ import time
6
+ import json
7
+ import runpy
8
+ import urllib.request
9
+ from pathlib import Path
10
+ from typing import Tuple
11
+
12
+
13
+ #----------------------------------------------------------------------------------------------------------------------
14
+
15
+ class DebugLog:
16
+ __colors = {
17
+ 'black': '\033[30m',
18
+ 'red': '\033[31m',
19
+ 'green': '\033[32m',
20
+ 'yellow': '\033[33m',
21
+ 'blue': '\033[34m',
22
+ 'magenta': '\033[35m',
23
+ 'cyan': '\033[36m',
24
+ 'white': '\033[37m',
25
+ 'orange': '\033[38;5;214m',
26
+ 'bright_black': '\033[90m',
27
+ 'bright_red': '\033[91m',
28
+ 'bright_green': '\033[92m',
29
+ 'bright_yellow': '\033[93m',
30
+ 'bright_blue': '\033[94m',
31
+ 'bright_magenta': '\033[95m',
32
+ 'bright_cyan': '\033[96m',
33
+ 'bright_white': '\033[97m',
34
+ 'gray_yellow': '\033[38;5;186m',
35
+ 'reset': '\033[0m',
36
+ }
37
+
38
+
39
+ def __supports_color() -> bool:
40
+ if not sys.stdout.isatty():
41
+ return False
42
+ if not sys.platform.startswith('win'):
43
+ return True
44
+ term = os.environ.get('TERM', '')
45
+ if 'xterm' in term or 'ansi' in term or 'cygwin' in term:
46
+ return True
47
+ if os.environ.get('ANSICON') or os.environ.get('WT_SESSION') or os.environ.get('TERM_PROGRAM', '') == 'vscode':
48
+ return True
49
+ return False
50
+
51
+
52
+ __enable_colors = __supports_color()
53
+
54
+
55
+ @staticmethod
56
+ def log(msg: str, color=None, end='\n', flush=False) -> None:
57
+ if color and DebugLog.__enable_colors:
58
+ print(DebugLog.__colors[color], msg, DebugLog.__colors['reset'], sep='', end=end, flush=flush)
59
+ else:
60
+ print(msg, end=end, flush=flush)
61
+
62
+
63
+ @staticmethod
64
+ def new_line() -> None:
65
+ print()
66
+
67
+
68
+ @staticmethod
69
+ def sep_by_sides(left_side: str, right_side: str = '', color=None, end='\n', flush=False) -> None:
70
+ width = shutil.get_terminal_size().columns
71
+ space = width - len(left_side) - len(right_side) - 1
72
+ if space > 0:
73
+ msg = f'\r{left_side:{len(left_side) + space}}{right_side}'
74
+ else:
75
+ short_left_side = left_side[:14] + '...' + left_side[14 + 3 + 2 - space:] + ' '
76
+ msg = f'\r{short_left_side}{right_side}'
77
+ DebugLog.log(msg, color=color, end=end, flush=flush)
78
+
79
+ #----------------------------------------------------------------------------------------------------------------------
80
+
81
+ class DevToolsLoader:
82
+ EXPECTED_JSON_VERSION = '0.1.0'
83
+ LOAD_RETRY_QTY = 5
84
+
85
+ PYTHON_PLATFORMS = [
86
+ 'win32', # Windows 32-bit
87
+ 'win_amd64', # Windows 64-bit
88
+ 'win_arm64', # Windows ARM64
89
+ 'manylinux1_x86_64', # Linux 64-bit (old)
90
+ 'manylinux2010_x86_64', # Linux 64-bit
91
+ 'manylinux2014_x86_64', # Linux 64-bit
92
+ 'manylinux1_i686', # Linux 32-bit
93
+ 'manylinux2010_i686', # Linux 32-bit
94
+ 'manylinux2014_i686', # Linux 32-bit
95
+ 'manylinux2014_aarch64', # Linux ARM64
96
+ 'manylinux2014_armv7l', # Linux ARM32
97
+ 'macosx_10_9_x86_64', # macOS Intel
98
+ 'macosx_11_0_arm64', # macOS Apple Silicon
99
+ ]
100
+
101
+ VSCODE_PLATFORMS = [
102
+ 'win32-x64',
103
+ 'win32-arm64',
104
+ 'linux-x64',
105
+ 'linux-arm64',
106
+ 'linux-armhf',
107
+ 'alpine-x64',
108
+ 'alpine-arm64',
109
+ 'darwin-x64',
110
+ 'darwin-arm64',
111
+ ]
112
+
113
+
114
+ def __init__(self, json_path: Path, output_path: Path = None, clean: bool = False):
115
+ if json_path:
116
+ json_path = Path(json_path)
117
+ if json_path.exists():
118
+ with open(json_path, 'r', encoding='utf-8') as f:
119
+ raw_json = f.read()
120
+ raw_json = re.sub(r'.*//.*', '', raw_json)
121
+ raw_json = re.sub(r'/\*[\s\S]*?\*/', '', raw_json)
122
+ self.__config = json.loads(raw_json)
123
+ else:
124
+ raise ValueError(f'JSON file \'{json_path}\' doesn\'t exist')
125
+
126
+ if self.__config['version'] != DevToolsLoader.EXPECTED_JSON_VERSION:
127
+ raise ValueError(f'incompatible JSON version - \'{self.__config["version"]}\', expected - \'{DevToolsLoader.EXPECTED_JSON_VERSION}\'')
128
+ else:
129
+ raise ValueError(f'json path required parameter')
130
+
131
+ if output_path:
132
+ self.__output_path = Path(output_path)
133
+ else:
134
+ self.__output_path = Path(os.getcwd()) / Path('output')
135
+ self.__clean = clean
136
+
137
+
138
+ @staticmethod
139
+ def __ver_2_tuple(ver_mmpb: str) -> Tuple[int]:
140
+ ver_mmp = ver_mmpb.split('-')[0]
141
+ return tuple(int(x) for x in ver_mmp.split('.'))
142
+
143
+
144
+ @staticmethod
145
+ def __load_file(url: str, dest_path: Path, deep: int = 0) -> None:
146
+ dest_path = Path(dest_path)
147
+ temp_path = Path(str(dest_path) + '.part')
148
+ tree_str = ''
149
+ if deep > 0:
150
+ tree_str = '└' + (deep * '────')[1:-1] + ' '
151
+ left_side = f'{tree_str}{dest_path}'
152
+
153
+ if dest_path.exists():
154
+ DebugLog.sep_by_sides(left_side, 'EXISTS', color='bright_black', end='', flush=True)
155
+ else:
156
+ DebugLog.sep_by_sides(left_side, color='gray_yellow', end='', flush=True)
157
+ start_time = time.time()
158
+ for attempt in range(DevToolsLoader.LOAD_RETRY_QTY):
159
+ try:
160
+ with urllib.request.urlopen(url) as response, open(temp_path, 'wb') as out_file:
161
+ total_size = int(response.headers['Content-Length'], 0)
162
+ block_size = 128 * 1024
163
+ download_size = 0
164
+ while download_size < total_size:
165
+ chunk = response.read(block_size)
166
+ out_file.write(chunk)
167
+ download_size += len(chunk)
168
+ percent = download_size * 100 // total_size
169
+ minutes, seconds = divmod(int(time.time() - start_time), 60)
170
+ left_side = f'{tree_str}{dest_path}'
171
+ right_side = f'{percent}% {download_size // 1024:8}KB {minutes:02d}:{seconds:02d}'
172
+ DebugLog.sep_by_sides(left_side, right_side, color='gray_yellow' if percent < 100 else 'bright_green', end='', flush=True)
173
+ temp_path.rename(dest_path)
174
+ break
175
+ except (ConnectionResetError, urllib.error.URLError):
176
+ DebugLog.sep_by_sides(left_side, 'FAILED', color='red')
177
+ time.sleep(2)
178
+ else:
179
+ raise RuntimeError(f'failed to load \'{url}\'')
180
+
181
+ DebugLog.new_line()
182
+
183
+
184
+ def __load_python(self, cfg: dict):
185
+ # make dir
186
+ target_path = self.__output_path / Path(f'python-{cfg["platform"]}-{cfg["version"]}')
187
+ if self.__clean:
188
+ DebugLog.log(f'>>> Cleaning output \'{target_path}\'', color='bright_cyan')
189
+ shutil.rmtree(target_path, ignore_errors=True)
190
+ if not target_path.exists():
191
+ target_path.mkdir(parents=True, exist_ok=True)
192
+ DebugLog.log(f'>>> Create dir \'{target_path}\'', color='bright_cyan')
193
+
194
+ # load installer
195
+ if cfg['installer'] == 'load':
196
+ try:
197
+ plat, arch = cfg['platform'].split('_')
198
+ arch = '-' + arch
199
+ except ValueError:
200
+ plat = cfg['platform']
201
+ arch = ''
202
+ if plat.startswith('win'):
203
+ ver = cfg['version']
204
+ file_name = f'python-{ver}{arch}.exe'
205
+ dest = target_path / Path(file_name)
206
+ url = f'https://www.python.org/ftp/python/{ver}/{file_name}'
207
+ DebugLog.log(f'>>> Loading python {ver} for platform {cfg["platform"]}', color='bright_cyan')
208
+ DevToolsLoader.__load_file(url, dest)
209
+ else:
210
+ DebugLog.log(f'>>> Load python for Linux is not supported yet', color='red')
211
+ # ver_major = ver.split('.')[0] + '.' + ver.split('.')[1]
212
+ # file_name = f'python{ver_major}_{ver}_{arch}.deb'
213
+ # dest = VSCODE_PATH / Path(file_name)
214
+ # url = f'https://deb.debian.org/debian/pool/main/p/python{ver_major}/{file_name}'
215
+
216
+ # load packages
217
+ if cfg.get('packages'):
218
+ for pkg in cfg['packages']:
219
+ pkg_name = pkg['name']
220
+ if pkg['version'] != 'latest':
221
+ pkg_name += f'=={pkg["version"]}'
222
+
223
+ pip_args = [
224
+ 'pip', 'download', pkg_name,
225
+ '--only-binary=:all:', '-q', '--disable-pip-version-check',
226
+ '--python-version', cfg['version'],
227
+ '--platform', cfg['platform'],
228
+ '-d', str(target_path)
229
+ ]
230
+
231
+ DebugLog.log(f'>>> Loading python package \'{pkg_name}\'', color='bright_cyan')
232
+ old_argv = sys.argv
233
+ sys.argv = pip_args
234
+
235
+ try:
236
+ runpy.run_module('pip', run_name='__main__')
237
+ sys.argv
238
+ except SystemExit as e:
239
+ if e.code != 0:
240
+ raise RuntimeWarning(f'failed to install {pkg_name} for platform {cfg["platform"]}')
241
+ finally:
242
+ sys.argv = old_argv
243
+
244
+ #save setup script
245
+ setup = ''
246
+ setup_path = target_path / Path('setup' + ('.bat' if cfg["platform"].startswith('win') else '.sh'))
247
+ for root, dirs, files in os.walk(target_path):
248
+ for file in files:
249
+ if Path(file).suffix == '.whl':
250
+ setup += f'pip install --no-index --find-links=./ {file}\n'
251
+ DebugLog.log(f'>>> Save Python setup script to \'{setup_path}\'', color='bright_cyan')
252
+ with open(setup_path, 'w', encoding='utf-8') as f:
253
+ f.write(setup)
254
+
255
+
256
+ @staticmethod
257
+ def __get_extension_info(uid: str, version: str, engine: str, platform: str) -> dict:
258
+ api_url = 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery'
259
+
260
+ query = {
261
+ 'filters': [{ 'criteria': [{'filterType': 7, 'value': f'{uid}'}] }],
262
+ 'flags': 0x1 | 0x2 | 0x10 | 0x80
263
+ }
264
+
265
+ headers = {
266
+ 'Content-Type': 'application/json',
267
+ 'Accept': 'application/json;api-version=3.0-preview.1',
268
+ 'User-Agent': 'Offline VSIX/1.0'
269
+ }
270
+
271
+ data = json.dumps(query).encode('utf-8')
272
+ req = urllib.request.Request(api_url, data, headers)
273
+
274
+ for attempt in range(DevToolsLoader.LOAD_RETRY_QTY):
275
+ try:
276
+ with urllib.request.urlopen(req) as resp:
277
+ info_list: list[dict] = json.load(resp)['results'][0]['extensions'][0]['versions']
278
+ # response_path = DevToolsLoader.TEMP_PATH / Path('response')
279
+ # if not response_path.exists():
280
+ # response_path.mkdir(parents=True, exist_ok=True)
281
+ # with open(response_path / Path(f'{uid}.json'), 'w', encoding='utf-8') as f:
282
+ # json.dump(info_list, f, indent=4)
283
+ # f.write('\n')
284
+
285
+ for info in info_list:
286
+ if info.get('targetPlatform') and platform is None:
287
+ raise RuntimeError(f'platform must be specified for {uid} platform')
288
+
289
+ if info.get('targetPlatform') is None or info['targetPlatform'] == platform:
290
+ for prop in info['properties']:
291
+ if prop['key'] == 'Microsoft.VisualStudio.Code.Engine':
292
+ ext_engine = prop['value'].replace('^', '')
293
+ break
294
+ else:
295
+ raise RuntimeError('failed to find Microsoft.VisualStudio.Code.Engine')
296
+
297
+ if engine == 'latest' or DevToolsLoader.__ver_2_tuple(ext_engine) <= DevToolsLoader.__ver_2_tuple(engine):
298
+ if version == 'latest' or version == info['version']:
299
+ return info
300
+ else:
301
+ raise RuntimeError(f'failed to find \'{uid}\' info')
302
+ except (ConnectionResetError, urllib.error.URLError):
303
+ time.sleep(2)
304
+ else:
305
+ raise RuntimeError(f'failed to connect \'{api_url}\'')
306
+
307
+
308
+ @staticmethod
309
+ def __load_vscode_extension_recursive(target_path: Path, uid: str, version: str, engine: str, platform: str, deep: int = 0, seen = None) -> None:
310
+ if seen is None:
311
+ seen = set()
312
+
313
+ if uid in seen:
314
+ return
315
+
316
+ seen.add(uid)
317
+
318
+ info = DevToolsLoader.__get_extension_info(uid, version, engine, platform)
319
+ for prop in info['files']:
320
+ if prop['assetType'] == 'Microsoft.VisualStudio.Services.VSIXPackage':
321
+ url = prop['source']
322
+ break
323
+ else:
324
+ raise RuntimeError(f'failed to find url')
325
+
326
+ #url = f'https://{uid.split('.')[0]}.gallery.vsassets.io/_apis/public/gallery/publisher/{uid.split('.')[0]}/extension/{uid.split('.')[1]}/{ver}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage'
327
+ #url = f'https://marketplace.visualstudio.com/_apis/public/gallery/publishers/{publisher}/vsextensions/{name}/{ver}/vspackage'
328
+
329
+ ver = info['version']
330
+ plat = info.get('targetPlatform')
331
+ plat = f'-{plat}' if plat else ''
332
+ file_name = f'{uid}-{ver}{plat}.vsix'
333
+ dest = target_path / Path(file_name)
334
+
335
+ DevToolsLoader.__load_file(url, dest, deep)
336
+
337
+ for prop in info['properties']:
338
+ if prop['key'] == 'Microsoft.VisualStudio.Code.ExtensionDependencies' or prop['key'] == 'Microsoft.VisualStudio.Code.ExtensionPack':
339
+ for dep_uid in prop['value'].split(','):
340
+ if dep_uid != '':
341
+ DevToolsLoader.__load_vscode_extension_recursive(target_path, dep_uid, 'latest', engine, platform, deep + 1, seen)
342
+
343
+
344
+ def __load_vscode(self, cfg: dict) -> None:
345
+ # make dir
346
+ target_path = self.__output_path / Path(f'vscode-{cfg["platform"]}-{cfg["version"]}')
347
+ if self.__clean:
348
+ DebugLog.log(f'>>> Cleaning output \'{target_path}\'', color='bright_cyan')
349
+ shutil.rmtree(target_path, ignore_errors=True)
350
+ if not target_path.exists():
351
+ target_path.mkdir(parents=True, exist_ok=True)
352
+ DebugLog.log(f'>>> Create dir \'{target_path}\'', color='bright_cyan')
353
+
354
+ # load installer
355
+ if cfg['installer'] == 'load':
356
+ plat = cfg['platform']
357
+ engine = cfg['version']
358
+ if plat.startswith('win'):
359
+ suffix = '.exe'
360
+ elif plat.startswith('darwin'):
361
+ suffix = '.zip'
362
+ elif plat.startswith('linux'):
363
+ suffix = '.tar.gz'
364
+ else:
365
+ suffix = None
366
+ DebugLog.log(f'>>> Load vscode for {plat} is not supported yet', color='red')
367
+ if suffix:
368
+ url_plat = plat if plat != 'darwin-x64' else 'darwin'
369
+ url = f'https://update.code.visualstudio.com/{engine}/{url_plat}/stable'
370
+ dest = target_path / Path(f'vscode-{engine}-{plat}{suffix}')
371
+ DebugLog.log(f'>>> Loading vscode {engine} for platform {plat}', color='bright_cyan')
372
+ DevToolsLoader.__load_file(url, dest)
373
+
374
+ # load extensions
375
+ if cfg.get('extensions'):
376
+ for ext in cfg['extensions']:
377
+ DevToolsLoader.__load_vscode_extension_recursive(target_path, ext['uid'], ext['version'], cfg['version'], cfg['platform'])
378
+
379
+ setup = ''
380
+ setup_path = target_path / Path('setup' + ('.bat' if cfg["platform"].startswith('win') else '.sh'))
381
+ for root, dirs, files in os.walk(target_path):
382
+ for file in files:
383
+ if Path(file).suffix == '.vsix':
384
+ setup += f'code --force --install-extension {file}\n'
385
+ DebugLog.log(f'>>> Save VSCode setup script to \'{setup_path}\'', color='bright_cyan')
386
+ with open(setup_path, 'w', encoding='utf-8') as f:
387
+ f.write(setup)
388
+
389
+
390
+ def run(self) -> None:
391
+ for cfg in self.__config['targets']:
392
+ if cfg['type'] == 'python':
393
+ self.__load_python(cfg)
394
+ elif cfg['type'] == 'vscode':
395
+ self.__load_vscode(cfg)
396
+ else:
397
+ raise ValueError(f'invalid target type \'{cfg["type"]}\'')
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: dev_tools_loader
3
+ Version: 0.1.0
4
+ Summary: Development tools loader
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://gitlab.com/karma_electronics/desktop/dev_tools_loader
7
+ Project-URL: Repository, https://gitlab.com/karma_electronics/desktop/dev_tools_loader.git
8
+ Requires-Python: >=3.6
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # Development Tools Loader
14
+
15
+ A CLI tool for automated downloading of development tools:
16
+
17
+ - VS Code installers and extensions (`.vsix` files).
18
+ - Python installers and pip packages;
19
+
20
+ The tool ensures version compatibility, handles dependencies, and supports repeatable configurations via JSON.
21
+
22
+
23
+ ## Key Features
24
+
25
+ - **Version control**: Ensures VS Code extension versions match the target VS Code engine version.
26
+ - **Package bundles**: Supports downloading multiple packages/extensions in a single config.
27
+ - **Dependency handling**: Automatically resolves and downloads dependencies for VS Code extensions.
28
+ - **Resilient downloads**: Automatically retries on connection loss.
29
+ - **Repeatable setups**: JSON-based configuration enables reproducible environment setups.
30
+ - **Flexible versioning**: Supports `"latest"` for extensions and packages.
31
+
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install dev-tools-loader
37
+ ```
38
+
39
+
40
+ ## Running
41
+
42
+ Once installed, you can run the tool from the command line using the JSON configuration file.
43
+
44
+ ```bash
45
+ dev_tools_loader -j path/to/config.json
46
+ ```
47
+
48
+
49
+ ## Command‑Line Options
50
+
51
+ - **`-j`, `--json-path` *`<json_config_path>`*** **(required)** Specifies the path to the JSON configuration file that defines download targets.
52
+ - **`-o`, `--output-path` *`<output_dir>`*** Sets the output directory where downloaded files will be saved.
53
+ - **`-c`, `--clean`** If specified, deletes files in the target output directory before starting the download process.
54
+ - **`-h`, `--help`** Displays the help message with a summary of all available options and exits.
55
+ - **`--version`** Prints the current version of the `dev-tools-loader` package and exits.
56
+
57
+
58
+ ## Example Config
59
+
60
+ ```json
61
+ {
62
+ "version": "0.1.0",
63
+ "targets": [
64
+ {
65
+ "type": "python",
66
+ "platform": "win_amd64",
67
+ "version": "3.12.0",
68
+ "installer": "load",
69
+ "packages": [
70
+ {
71
+ "name": "compiledb",
72
+ "version": "0.10.6"
73
+ },
74
+ {
75
+ "name": "requests",
76
+ "version": "latest"
77
+ }
78
+ ]
79
+ },
80
+ {
81
+ "type": "vscode",
82
+ "platform": "win32-x64",
83
+ "version": "1.96.0",
84
+ "installer": "load",
85
+ "extensions": [
86
+ {
87
+ "uid": "ms-vscode.cpptools",
88
+ "version": "1.28.0"
89
+ },
90
+ {
91
+ "uid": "ms-python.python",
92
+ "version": "latest"
93
+ }
94
+ ]
95
+ }
96
+ ]
97
+ }
98
+ ```
99
+
100
+
101
+ ## Configuration Fields
102
+
103
+ - `version` (str): Schema version.
104
+ - `targets` (list): List of download targets. Each target has:
105
+ - `type` (str): `"python"` or `"vscode"`.
106
+ - `platform` (str): Target platform (see supported platforms below).
107
+ - `version` (str): Version of the tool.
108
+ - `installer` (str): `"load"` to download installer or `"skip"`.
109
+ - `packages` (list, Python-only): List of pip packages to download.
110
+ - `name` (str): Package name.
111
+ - `version` (str): Package version (`"latest"` supported).
112
+ - `extensions` (list, VS Code-only): List of VS Code extensions to download.
113
+ - `uid` (str): Extension ID (e.g., `"ms-vscode.cpptools"`).
114
+ - `version` (str): Extension version (`"latest"` supported).
115
+
116
+
117
+ ## Supported Platforms Python
118
+
119
+ - `win32`
120
+ - `win_amd64`
121
+ - `win_arm64`
122
+ - `manylinux1_x86_64`
123
+ - `manylinux2010_x86_64`
124
+ - `manylinux2014_x86_64`
125
+ - `manylinux1_i686`
126
+ - `manylinux2010_i686`
127
+ - `manylinux2014_i686`
128
+ - `manylinux2014_aarch64`
129
+ - `manylinux2014_armv7l`
130
+ - `macosx_10_9_x86_64`
131
+ - `macosx_11_0_arm64`
132
+
133
+ ## Supported Platforms VS Code
134
+
135
+ - `win32-x64`
136
+ - `win32-arm64`
137
+ - `linux-x64`
138
+ - `linux-arm64`
139
+ - `linux-armhf`
140
+ - `alpine-x64`
141
+ - `alpine-arm64`
142
+ - `darwin-x64`
143
+ - `darwin-arm64`
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ dev_tools_loader/__init__.py
5
+ dev_tools_loader/__main__.py
6
+ dev_tools_loader/cli.py
7
+ dev_tools_loader/dev_tools_loader.py
8
+ dev_tools_loader.egg-info/PKG-INFO
9
+ dev_tools_loader.egg-info/SOURCES.txt
10
+ dev_tools_loader.egg-info/dependency_links.txt
11
+ dev_tools_loader.egg-info/entry_points.txt
12
+ dev_tools_loader.egg-info/top_level.txt
13
+ tests/test_dev_tools_loader.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dev_tools_loader = dev_tools_loader.cli:main
@@ -0,0 +1 @@
1
+ dev_tools_loader
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dev_tools_loader"
7
+ version = "0.1.0"
8
+ description = "Development tools loader"
9
+ readme = "README.md"
10
+ requires-python = ">=3.6"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ dependencies = []
14
+
15
+ [project.scripts]
16
+ dev_tools_loader = "dev_tools_loader.cli:main"
17
+
18
+ [tool.setuptools]
19
+ packages = ["dev_tools_loader"]
20
+
21
+ [project.urls]
22
+ Homepage = "https://gitlab.com/karma_electronics/desktop/dev_tools_loader"
23
+ Repository = "https://gitlab.com/karma_electronics/desktop/dev_tools_loader.git"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,127 @@
1
+ import json
2
+ import pytest
3
+ from pathlib import Path
4
+ from unittest.mock import patch
5
+
6
+ from dev_tools_loader.dev_tools_loader import DevToolsLoader
7
+ from dev_tools_loader.cli import main
8
+
9
+
10
+ @pytest.fixture
11
+ def config_template():
12
+ return {
13
+ 'version': '0.1.0',
14
+ 'targets': [
15
+ {
16
+ 'type': 'python',
17
+ 'platform': 'none',
18
+ 'version': '3.12.0',
19
+ 'installer': "load",
20
+ 'packages': [
21
+ {
22
+ 'name': 'compiledb',
23
+ 'version': '0.10.6'
24
+ },
25
+ {
26
+ 'name': 'pyserial',
27
+ 'version': '3.2'
28
+ },
29
+ {
30
+ 'name': 'requests',
31
+ 'version': 'latest'
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ 'type': 'vscode',
37
+ 'platform': 'none',
38
+ 'version': '1.96.0',
39
+ 'installer': "load",
40
+ 'extensions': [
41
+ {
42
+ 'uid': 'ms-vscode.cpptools',
43
+ 'version': '1.28.0'
44
+ },
45
+ {
46
+ 'uid': 'ms-python.python',
47
+ 'version': 'latest'
48
+ },
49
+ {
50
+ 'uid': 'ms-python.debugpy',
51
+ 'version': 'latest'
52
+ },
53
+ {
54
+ 'uid': 'marus25.cortex-debug',
55
+ 'version': '1.12.0'
56
+ },
57
+ {
58
+ 'uid': 'streetsidesoftware.code-spell-checker-russian',
59
+ 'version': 'latest'
60
+ }
61
+ ]
62
+ }
63
+ ]
64
+ }
65
+
66
+
67
+ def save_json_config(json_path: Path, config: dict) -> None:
68
+ json_path = Path(json_path)
69
+ with open(json_path, 'w', encoding='utf-8') as f:
70
+ json.dump(config, f, indent=4)
71
+ f.write('\n')
72
+
73
+
74
+ @pytest.mark.fast
75
+ def test_cli_help():
76
+ print()
77
+ with patch('sys.argv', ['dev_tools_loader', '-h']):
78
+ with pytest.raises(SystemExit):
79
+ main()
80
+
81
+
82
+ @pytest.mark.fast
83
+ def test_cli_version():
84
+ print()
85
+ with patch('sys.argv', ['dev_tools_loader', '--version']):
86
+ with pytest.raises(SystemExit):
87
+ main()
88
+
89
+
90
+ @pytest.mark.fast
91
+ def test_cli_invalid_config():
92
+ print()
93
+ with patch('sys.argv', ['dev_tools_loader']):
94
+ with pytest.raises(SystemExit):
95
+ main()
96
+
97
+
98
+ @pytest.mark.fast
99
+ def test_config_example(tmp_path):
100
+ print()
101
+ config_path = Path(__file__).parent / Path('data/config_example.json')
102
+ with patch('sys.argv', ['dev_tools_loader', '-j', str(config_path), '-o', str(tmp_path)]):
103
+ main()
104
+
105
+
106
+ @pytest.mark.parametrize('platform', DevToolsLoader.PYTHON_PLATFORMS)
107
+ def test_python_load(config_template, platform, tmp_path):
108
+ config = config_template
109
+ config['targets'][0]['platform'] = platform
110
+ config['targets'].pop(1)
111
+ json_path = tmp_path / Path(f'config_python_{platform}.json')
112
+ save_json_config(json_path, config)
113
+ print()
114
+ dtl = DevToolsLoader(json_path, tmp_path)
115
+ dtl.run()
116
+
117
+
118
+ @pytest.mark.parametrize('platform', DevToolsLoader.VSCODE_PLATFORMS)
119
+ def test_vscode_load(config_template, platform, tmp_path):
120
+ config = config_template
121
+ config['targets'][1]['platform'] = platform
122
+ config['targets'].pop(0)
123
+ json_path = tmp_path / Path(f'config_vscode_{platform}.json')
124
+ save_json_config(json_path, config)
125
+ print()
126
+ dtl = DevToolsLoader(json_path, tmp_path)
127
+ dtl.run()