holoscan-cli 2.9.0__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.
Files changed (39) hide show
  1. holoscan_cli/__init__.py +35 -0
  2. holoscan_cli/__main__.py +164 -0
  3. holoscan_cli/common/argparse_types.py +156 -0
  4. holoscan_cli/common/artifact_sources.py +160 -0
  5. holoscan_cli/common/constants.py +119 -0
  6. holoscan_cli/common/dockerutils.py +521 -0
  7. holoscan_cli/common/enum_types.py +49 -0
  8. holoscan_cli/common/exceptions.py +126 -0
  9. holoscan_cli/common/sdk_utils.py +195 -0
  10. holoscan_cli/common/utils.py +137 -0
  11. holoscan_cli/logging.json +37 -0
  12. holoscan_cli/nics/__init__.py +15 -0
  13. holoscan_cli/nics/nics.py +33 -0
  14. holoscan_cli/package-source.json +32 -0
  15. holoscan_cli/packager/__init__.py +15 -0
  16. holoscan_cli/packager/arguments.py +148 -0
  17. holoscan_cli/packager/config_reader.py +180 -0
  18. holoscan_cli/packager/container_builder.py +426 -0
  19. holoscan_cli/packager/manifest_files.py +217 -0
  20. holoscan_cli/packager/models.py +90 -0
  21. holoscan_cli/packager/package_command.py +197 -0
  22. holoscan_cli/packager/packager.py +124 -0
  23. holoscan_cli/packager/parameters.py +603 -0
  24. holoscan_cli/packager/platforms.py +426 -0
  25. holoscan_cli/packager/templates/Dockerfile.jinja2 +479 -0
  26. holoscan_cli/packager/templates/dockerignore +92 -0
  27. holoscan_cli/packager/templates/tools.sh +414 -0
  28. holoscan_cli/py.typed +14 -0
  29. holoscan_cli/runner/__init__.py +15 -0
  30. holoscan_cli/runner/resources.py +185 -0
  31. holoscan_cli/runner/run_command.py +207 -0
  32. holoscan_cli/runner/runner.py +340 -0
  33. holoscan_cli/version/__init__.py +15 -0
  34. holoscan_cli/version/version.py +53 -0
  35. holoscan_cli-2.9.0.dist-info/LICENSE +201 -0
  36. holoscan_cli-2.9.0.dist-info/METADATA +102 -0
  37. holoscan_cli-2.9.0.dist-info/RECORD +39 -0
  38. holoscan_cli-2.9.0.dist-info/WHEEL +4 -0
  39. holoscan_cli-2.9.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,603 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ import logging
16
+ import os
17
+ import platform
18
+ from pathlib import Path
19
+ from typing import Any, Optional
20
+
21
+ from ..common.constants import SDK, Constants, DefaultValues
22
+ from ..common.dockerutils import parse_docker_image_name_and_tag
23
+ from ..common.enum_types import (
24
+ ApplicationType,
25
+ Arch,
26
+ Platform,
27
+ PlatformConfiguration,
28
+ SdkType,
29
+ )
30
+ from ..common.exceptions import InvalidTagValueError, UnknownApplicationTypeError
31
+
32
+
33
+ class PlatformParameters:
34
+ def __init__(
35
+ self,
36
+ platform: Platform,
37
+ platform_config: PlatformConfiguration,
38
+ tag: str,
39
+ version: str,
40
+ ) -> None:
41
+ self._logger = logging.getLogger("platform.parameters")
42
+ self._platform: Platform = platform
43
+ self._platform_config: PlatformConfiguration = platform_config
44
+ self._arch: Arch = SDK.PLATFORM_MAPPINGS[platform]
45
+ self._tag_prefix: Optional[str]
46
+ self._version: Optional[str]
47
+
48
+ (self._tag_prefix, self._version) = parse_docker_image_name_and_tag(tag)
49
+
50
+ if self._tag_prefix is None:
51
+ raise InvalidTagValueError(
52
+ f"'{tag}' is not a valid Docker tag. Format: name[:tag]"
53
+ )
54
+
55
+ if self._version is None:
56
+ self._version = version
57
+
58
+ self._data: dict[str, Any] = {}
59
+ self._data["tag"] = tag
60
+ self._data["base_image"] = None
61
+ self._data["build_image"] = None
62
+ self._data["holoscan_sdk_file"] = None
63
+ self._data["holoscan_sdk_filename"] = None
64
+ self._data["monai_deploy_sdk_file"] = None
65
+ self._data["monai_deploy_sdk_filename"] = None
66
+ self._data["custom_base_image"] = False
67
+ self._data["custom_holoscan_sdk"] = False
68
+ self._data["custom_monai_deploy_sdk"] = False
69
+ self._data["target_arch"] = "aarch64" if self._arch == Arch.arm64 else "x86_64"
70
+ self._data["cuda_deb_arch"] = "sbsa" if self._arch == Arch.arm64 else "x86_64"
71
+ self._data["holoscan_deb_arch"] = (
72
+ "arm64" if self._arch == Arch.arm64 else "amd64"
73
+ )
74
+ self._data["gpu_type"] = self.platform_config.value
75
+
76
+ @property
77
+ def tag(self) -> str:
78
+ return (
79
+ f"{self._tag_prefix}-"
80
+ f"{self.platform.value}-"
81
+ f"{self.platform_config.value}-"
82
+ f"{self.platform_arch.value}:"
83
+ f"{self.version}"
84
+ ).replace("/", "-")
85
+
86
+ @property
87
+ def tag_prefix(self) -> str:
88
+ return self._tag_prefix
89
+
90
+ @property
91
+ def custom_base_image(self) -> Optional[str]:
92
+ return self._data["custom_base_image"]
93
+
94
+ @custom_base_image.setter
95
+ def custom_base_image(self, value: str):
96
+ self._data["custom_base_image"] = value
97
+
98
+ @property
99
+ def custom_holoscan_sdk(self) -> Optional[str]:
100
+ return self._data["custom_holoscan_sdk"]
101
+
102
+ @custom_holoscan_sdk.setter
103
+ def custom_holoscan_sdk(self, value: str):
104
+ self._data["custom_holoscan_sdk"] = value
105
+
106
+ @property
107
+ def custom_monai_deploy_sdk(self) -> Optional[str]:
108
+ return self._data["custom_monai_deploy_sdk"]
109
+
110
+ @custom_monai_deploy_sdk.setter
111
+ def custom_monai_deploy_sdk(self, value: str):
112
+ self._data["custom_monai_deploy_sdk"] = value
113
+
114
+ @property
115
+ def base_image(self) -> Optional[str]:
116
+ return self._data["base_image"]
117
+
118
+ @base_image.setter
119
+ def base_image(self, value: str):
120
+ self._data["base_image"] = value
121
+
122
+ @property
123
+ def build_image(self) -> Optional[str]:
124
+ return self._data["build_image"]
125
+
126
+ @build_image.setter
127
+ def build_image(self, value: str):
128
+ self._data["build_image"] = value
129
+
130
+ @property
131
+ def holoscan_sdk_file(self) -> Optional[Path]:
132
+ return self._data["holoscan_sdk_file"]
133
+
134
+ @holoscan_sdk_file.setter
135
+ def holoscan_sdk_file(self, value: Path):
136
+ self._data["holoscan_sdk_file"] = value
137
+ if value is not None and hasattr(value, "name"):
138
+ self._data["holoscan_sdk_filename"] = value.name
139
+ elif value is not None:
140
+ self._data["holoscan_sdk_filename"] = value
141
+
142
+ @property
143
+ def monai_deploy_sdk_file(self) -> Optional[Path]:
144
+ return self._data["monai_deploy_sdk_file"]
145
+
146
+ @monai_deploy_sdk_file.setter
147
+ def monai_deploy_sdk_file(self, value: Path):
148
+ self._data["monai_deploy_sdk_file"] = value
149
+ if value is not None and hasattr(value, "name"):
150
+ self._data["monai_deploy_sdk_filename"] = value.name
151
+
152
+ @property
153
+ def version(self) -> str:
154
+ return self._version
155
+
156
+ @property
157
+ def health_probe(self) -> Optional[Path]:
158
+ return self._data.get("health_probe", None)
159
+
160
+ @health_probe.setter
161
+ def health_probe(self, value: Optional[Path]):
162
+ self._data["health_probe"] = value
163
+
164
+ @property
165
+ def platform_arch(self) -> Arch:
166
+ return self._arch
167
+
168
+ @property
169
+ def docker_arch(self) -> str:
170
+ return self._arch.value
171
+
172
+ @property
173
+ def platform(self) -> Platform:
174
+ return self._platform
175
+
176
+ @property
177
+ def platform_config(self) -> PlatformConfiguration:
178
+ return self._platform_config
179
+
180
+ @property
181
+ def to_jinja(self) -> dict[str, Any]:
182
+ return self._data
183
+
184
+ @property
185
+ def same_arch_as_system(self) -> bool:
186
+ return (platform.machine() == "aarch64" and self._arch == Arch.arm64) or (
187
+ platform.machine() == "x86_64" and self._arch == Arch.amd64
188
+ )
189
+
190
+ @property
191
+ def cuda_deb_arch(self) -> str:
192
+ return self._data["cuda_deb_arch"]
193
+
194
+ @property
195
+ def holoscan_deb_arch(self) -> str:
196
+ return self._data["holoscan_deb_arch"]
197
+
198
+ @property
199
+ def target_arch(self) -> str:
200
+ return self._data["target_arch"]
201
+
202
+
203
+ class PlatformBuildResults:
204
+ def __init__(self, parameters: PlatformParameters):
205
+ self._parameters = parameters
206
+ self._docker_tag: Optional[str] = None
207
+ self._tarball_filenaem: Optional[str] = None
208
+ self._succeeded = False
209
+ self._error: Optional[str] = None
210
+
211
+ @property
212
+ def parameters(self) -> PlatformParameters:
213
+ return self._parameters
214
+
215
+ @property
216
+ def error(self) -> Optional[str]:
217
+ return self._error
218
+
219
+ @error.setter
220
+ def error(self, value: Optional[str]):
221
+ self._error = value
222
+
223
+ @property
224
+ def docker_tag(self) -> Optional[str]:
225
+ return self._docker_tag
226
+
227
+ @docker_tag.setter
228
+ def docker_tag(self, value: Optional[str]):
229
+ self._docker_tag = value
230
+
231
+ @property
232
+ def tarball_filenaem(self) -> Optional[str]:
233
+ return self._tarball_filenaem
234
+
235
+ @tarball_filenaem.setter
236
+ def tarball_filenaem(self, value: Optional[str]):
237
+ self._tarball_filenaem = value
238
+
239
+ @property
240
+ def succeeded(self) -> bool:
241
+ return self._succeeded
242
+
243
+ @succeeded.setter
244
+ def succeeded(self, value: bool):
245
+ self._succeeded = value
246
+
247
+
248
+ class PackageBuildParameters:
249
+ """
250
+ Parameters required for building the Docker image with Jinja template.
251
+ """
252
+
253
+ def __init__(self):
254
+ self._logger = logging.getLogger("packager.parameters")
255
+ self._data = {}
256
+ self._data["app_dir"] = DefaultValues.HOLOSCAN_APP_DIR
257
+ self._data["lib_dir"] = DefaultValues.HOLOSCAN_LIB_DIR
258
+ self._data["config_file_path"] = DefaultValues.HOLOSCAN_CONFIG_PATH
259
+ self._data["docs_dir"] = DefaultValues.HOLOSCAN_DOCS_DIR
260
+ self._data["logs_dir"] = DefaultValues.HOLOSCAN_LOGS_DIR
261
+ self._data["full_input_path"] = DefaultValues.WORK_DIR / DefaultValues.INPUT_DIR
262
+ self._data["full_output_path"] = (
263
+ DefaultValues.WORK_DIR / DefaultValues.OUTPUT_DIR
264
+ )
265
+ self._data["input_dir"] = DefaultValues.INPUT_DIR
266
+ self._data["models_dir"] = DefaultValues.MODELS_DIR
267
+ self._data["output_dir"] = DefaultValues.OUTPUT_DIR
268
+ self._data["timeout"] = DefaultValues.TIMEOUT
269
+ self._data["working_dir"] = DefaultValues.WORK_DIR
270
+ self._data["app_json"] = DefaultValues.APP_MANIFEST_PATH
271
+ self._data["pkg_json"] = DefaultValues.PKG_MANIFEST_PATH
272
+ self._data["username"] = DefaultValues.USERNAME
273
+ self._data["build_cache"] = DefaultValues.BUILD_CACHE_DIR
274
+ self._data["uid"] = os.getuid()
275
+ self._data["gid"] = os.getgid()
276
+ self._data["tarball_output"] = None
277
+ self._data["cmake_args"] = ""
278
+ self._data["includes"] = []
279
+ self._data["additional_lib_paths"] = ""
280
+
281
+ self._data["application_directory"] = None
282
+ self._data["application_type"] = None
283
+ self._data["application"] = None
284
+ self._data["app_config_file_path"] = None
285
+ self._data["command"] = None
286
+ self._data["no_cache"] = False
287
+ self._data["pip_packages"] = None
288
+ self._data["requirements_file_path"] = None
289
+ self._data["holoscan_sdk_version"] = None
290
+ self._data["monai_deploy_app_sdk_version"] = None
291
+ self._data["title"] = None
292
+ self._data["version"] = None
293
+
294
+ self._additional_libs = []
295
+
296
+ @property
297
+ def build_cache(self) -> int:
298
+ return self._data["build_cache"]
299
+
300
+ @build_cache.setter
301
+ def build_cache(self, value: int):
302
+ self._data["build_cache"] = value
303
+
304
+ @property
305
+ def full_input_path(self) -> str:
306
+ return self._data["full_input_path"]
307
+
308
+ @property
309
+ def full_output_path(self) -> str:
310
+ return self._data["full_output_path"]
311
+
312
+ @property
313
+ def docs_dir(self) -> str:
314
+ return self._data["docs_dir"]
315
+
316
+ @property
317
+ def logs_dir(self) -> str:
318
+ return self._data["logs_dir"]
319
+
320
+ @property
321
+ def tarball_output(self) -> int:
322
+ return self._data["tarball_output"]
323
+
324
+ @tarball_output.setter
325
+ def tarball_output(self, value: int):
326
+ self._data["tarball_output"] = value
327
+
328
+ @property
329
+ def cmake_args(self) -> str:
330
+ return self._data["cmake_args"]
331
+
332
+ @cmake_args.setter
333
+ def cmake_args(self, value: str):
334
+ self._data["cmake_args"] = value.strip('"') if value is not None else ""
335
+
336
+ @property
337
+ def gid(self) -> int:
338
+ return self._data["gid"]
339
+
340
+ @gid.setter
341
+ def gid(self, value: int):
342
+ self._data["gid"] = value
343
+
344
+ @property
345
+ def uid(self) -> int:
346
+ return self._data["uid"]
347
+
348
+ @uid.setter
349
+ def uid(self, value: int):
350
+ self._data["uid"] = value
351
+
352
+ @property
353
+ def username(self) -> str:
354
+ return self._data["username"]
355
+
356
+ @username.setter
357
+ def username(self, value: str):
358
+ self._data["username"] = value
359
+
360
+ @property
361
+ def app_manifest_path(self):
362
+ return self._data["app_json"]
363
+
364
+ @property
365
+ def package_manifest_path(self):
366
+ return self._data["pkg_json"]
367
+
368
+ @property
369
+ def title(self):
370
+ return self._data["title"]
371
+
372
+ @title.setter
373
+ def title(self, value):
374
+ self._data["title"] = value
375
+
376
+ @property
377
+ def docs(self) -> Path:
378
+ return self._data.get("docs", None)
379
+
380
+ @docs.setter
381
+ def docs(self, value: Path):
382
+ if value is not None:
383
+ self._data["docs"] = value
384
+
385
+ @property
386
+ def models(self) -> dict[str, Path]:
387
+ return self._data.get("models", None)
388
+
389
+ @models.setter
390
+ def models(self, value: dict[str, Path]):
391
+ if value is not None:
392
+ self._data["models"] = value
393
+
394
+ @property
395
+ def pip_packages(self):
396
+ return self._data["pip_packages"]
397
+
398
+ @pip_packages.setter
399
+ def pip_packages(self, value):
400
+ self._data["pip_packages"] = value
401
+
402
+ @property
403
+ def no_cache(self):
404
+ return self._data["no_cache"]
405
+
406
+ @no_cache.setter
407
+ def no_cache(self, value):
408
+ self._data["no_cache"] = value
409
+
410
+ @property
411
+ def config_file_path(self):
412
+ return self._data["config_file_path"]
413
+
414
+ @property
415
+ def app_config_file_path(self):
416
+ return self._data["app_config_file_path"]
417
+
418
+ @app_config_file_path.setter
419
+ def app_config_file_path(self, value):
420
+ self._data["app_config_file_path"] = value
421
+
422
+ @property
423
+ def app_dir(self):
424
+ return self._data["app_dir"]
425
+
426
+ @property
427
+ def application(self) -> Path:
428
+ return self._data["application"]
429
+
430
+ @application.setter
431
+ def application(self, value: Path):
432
+ self._data["application"] = value
433
+ self._logger.info(f"Application: {self.application}")
434
+ self._application_type = self._detect_application_type()
435
+ self._data["application_type"] = self._application_type.name
436
+ self._logger.info(f"Detected application type: {self.application_type.value}")
437
+ self._data["application_directory"] = (
438
+ self.application
439
+ if os.path.isdir(self.application)
440
+ else Path(os.path.dirname(self.application))
441
+ )
442
+ requirements_file_path = self.application_directory / "requirements.txt"
443
+ if os.path.exists(requirements_file_path):
444
+ self._data["requirements_file_path"] = requirements_file_path
445
+ else:
446
+ self._data["requirements_file_path"] = None
447
+ self._data["command"] = self._set_app_command()
448
+ self._data["command_filename"] = os.path.basename(self.application)
449
+
450
+ @property
451
+ def command_filename(self) -> str:
452
+ return self._data["command_filename"]
453
+
454
+ @property
455
+ def command(self) -> str:
456
+ return self._data["command"]
457
+
458
+ @property
459
+ def application_directory(self) -> Path:
460
+ return self._data["application_directory"]
461
+
462
+ @property
463
+ def requirements_file_path(self) -> Path:
464
+ return self._data["requirements_file_path"]
465
+
466
+ @property
467
+ def version(self) -> str:
468
+ return self._data["version"]
469
+
470
+ @version.setter
471
+ def version(self, value: str):
472
+ self._data["version"] = value
473
+
474
+ @property
475
+ def timeout(self) -> int:
476
+ return self._data["timeout"]
477
+
478
+ @timeout.setter
479
+ def timeout(self, value: int):
480
+ self._data["timeout"] = value
481
+
482
+ @property
483
+ def working_dir(self) -> Path:
484
+ return self._data["working_dir"]
485
+
486
+ @property
487
+ def models_dir(self) -> Path:
488
+ return self._data["models_dir"]
489
+
490
+ @models_dir.setter
491
+ def models_dir(self, value: Path):
492
+ self._data["models_dir"] = value
493
+
494
+ @property
495
+ def input_dir(self) -> str:
496
+ return self._data["input_dir"]
497
+
498
+ @property
499
+ def output_dir(self) -> str:
500
+ return self._data["output_dir"]
501
+
502
+ @property
503
+ def application_type(self) -> ApplicationType:
504
+ return self._application_type
505
+
506
+ @property
507
+ def sdk(self) -> SdkType:
508
+ return self._data["sdk"]
509
+
510
+ @sdk.setter
511
+ def sdk(self, value: SdkType):
512
+ self._data["sdk"] = value
513
+ self._data["sdk_type"] = value.value
514
+
515
+ @property
516
+ def holoscan_sdk_version(self) -> str:
517
+ return self._data["holoscan_sdk_version"]
518
+
519
+ @holoscan_sdk_version.setter
520
+ def holoscan_sdk_version(self, value: str):
521
+ self._data["holoscan_sdk_version"] = value
522
+
523
+ @property
524
+ def monai_deploy_app_sdk_version(self) -> str:
525
+ return self._data["monai_deploy_app_sdk_version"]
526
+
527
+ @monai_deploy_app_sdk_version.setter
528
+ def monai_deploy_app_sdk_version(self, value: str):
529
+ self._data["monai_deploy_app_sdk_version"] = value
530
+
531
+ @property
532
+ def includes(self) -> str:
533
+ return self._data["includes"]
534
+
535
+ @includes.setter
536
+ def includes(self, value: str):
537
+ self._data["includes"] = value
538
+
539
+ @property
540
+ def additional_lib_paths(self) -> str:
541
+ """
542
+ Additional libraries that user wants to include in the package.
543
+ Stores the post-processed values to be injected into LD_LIBRARY_PATH and PYTHONPATH in the
544
+ Jinja2 template.
545
+ """
546
+ return self._data["additional_lib_paths"]
547
+
548
+ @additional_lib_paths.setter
549
+ def additional_lib_paths(self, value: str):
550
+ self._data["additional_lib_paths"] = value
551
+
552
+ @property
553
+ def additional_libs(self) -> list[str]:
554
+ """
555
+ Additional libraries that user wants to include in the package.
556
+ Stores paths entered from the command line before processing.
557
+ """
558
+ return self._additional_libs
559
+
560
+ @additional_libs.setter
561
+ def additional_libs(self, value: list[str]):
562
+ self._additional_libs = value
563
+
564
+ @property
565
+ def to_jinja(self) -> dict[str, Any]:
566
+ return self._data
567
+
568
+ def _detect_application_type(self) -> ApplicationType:
569
+ if os.path.isdir(self.application):
570
+ if os.path.exists(self.application / Constants.PYTHON_MAIN_FILE):
571
+ return ApplicationType.PythonModule
572
+ elif os.path.exists(self.application / Constants.CPP_CMAKELIST_FILE):
573
+ return ApplicationType.CppCMake
574
+ elif os.path.isfile(self.application):
575
+ if Path(self.application).suffix == ".py":
576
+ return ApplicationType.PythonFile
577
+ elif os.access(self.application, os.X_OK):
578
+ return ApplicationType.Binary
579
+
580
+ raise UnknownApplicationTypeError(
581
+ f"""\n\nUnable to determine application type. Please ensure the application path
582
+ contains one of the following:
583
+ \t- Python directory/module with '{Constants.PYTHON_MAIN_FILE}'
584
+ \t- Python file
585
+ \t- C++ source directory with '{Constants.CPP_CMAKELIST_FILE}'
586
+ \t- Binary file"""
587
+ )
588
+
589
+ def _set_app_command(self) -> str:
590
+ if self.application_type == ApplicationType.PythonFile:
591
+ return (
592
+ f'["{Constants.PYTHON_EXECUTABLE}", '
593
+ + f'"{os.path.join(self._data["app_dir"], os.path.basename(self.application))}"]'
594
+ )
595
+ elif self.application_type == ApplicationType.PythonModule:
596
+ return f'["{Constants.PYTHON_EXECUTABLE}", "{self._data["app_dir"]}"]'
597
+ elif self.application_type in [
598
+ ApplicationType.CppCMake,
599
+ ApplicationType.Binary,
600
+ ]:
601
+ return f'["{os.path.join(self._data["app_dir"], os.path.basename(self.application))}"]'
602
+
603
+ raise UnknownApplicationTypeError("Unsupported application type.")