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.
- holoscan_cli/__init__.py +35 -0
- holoscan_cli/__main__.py +164 -0
- holoscan_cli/common/argparse_types.py +156 -0
- holoscan_cli/common/artifact_sources.py +160 -0
- holoscan_cli/common/constants.py +119 -0
- holoscan_cli/common/dockerutils.py +521 -0
- holoscan_cli/common/enum_types.py +49 -0
- holoscan_cli/common/exceptions.py +126 -0
- holoscan_cli/common/sdk_utils.py +195 -0
- holoscan_cli/common/utils.py +137 -0
- holoscan_cli/logging.json +37 -0
- holoscan_cli/nics/__init__.py +15 -0
- holoscan_cli/nics/nics.py +33 -0
- holoscan_cli/package-source.json +32 -0
- holoscan_cli/packager/__init__.py +15 -0
- holoscan_cli/packager/arguments.py +148 -0
- holoscan_cli/packager/config_reader.py +180 -0
- holoscan_cli/packager/container_builder.py +426 -0
- holoscan_cli/packager/manifest_files.py +217 -0
- holoscan_cli/packager/models.py +90 -0
- holoscan_cli/packager/package_command.py +197 -0
- holoscan_cli/packager/packager.py +124 -0
- holoscan_cli/packager/parameters.py +603 -0
- holoscan_cli/packager/platforms.py +426 -0
- holoscan_cli/packager/templates/Dockerfile.jinja2 +479 -0
- holoscan_cli/packager/templates/dockerignore +92 -0
- holoscan_cli/packager/templates/tools.sh +414 -0
- holoscan_cli/py.typed +14 -0
- holoscan_cli/runner/__init__.py +15 -0
- holoscan_cli/runner/resources.py +185 -0
- holoscan_cli/runner/run_command.py +207 -0
- holoscan_cli/runner/runner.py +340 -0
- holoscan_cli/version/__init__.py +15 -0
- holoscan_cli/version/version.py +53 -0
- holoscan_cli-2.9.0.dist-info/LICENSE +201 -0
- holoscan_cli-2.9.0.dist-info/METADATA +102 -0
- holoscan_cli-2.9.0.dist-info/RECORD +39 -0
- holoscan_cli-2.9.0.dist-info/WHEEL +4 -0
- holoscan_cli-2.9.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,180 @@
|
|
|
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 os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from ..common.constants import DefaultValues, EnvironmentVariables
|
|
22
|
+
from ..common.exceptions import InvalidApplicationConfigurationError
|
|
23
|
+
from .manifest_files import ApplicationManifest, PackageManifest
|
|
24
|
+
from .parameters import PackageBuildParameters
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ApplicationConfiguration:
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._config = None
|
|
30
|
+
|
|
31
|
+
def read(self, path: Path):
|
|
32
|
+
"""Reads application configuration file
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
path (Path): path to the app configuration file
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
FileNotFoundError: when file does not exists or is not a file
|
|
39
|
+
InvalidApplicationConfigurationError: error reading file
|
|
40
|
+
"""
|
|
41
|
+
if not path.exists() or not path.is_file():
|
|
42
|
+
raise FileNotFoundError(
|
|
43
|
+
f"Specified application configuration file cannot be found: {path}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
with open(path) as app_manifest_file:
|
|
48
|
+
self._config = yaml.load(app_manifest_file, yaml.SafeLoader)
|
|
49
|
+
except Exception as ex:
|
|
50
|
+
raise InvalidApplicationConfigurationError(
|
|
51
|
+
f"Error reading application configuration file from '{path}'. Please check that "
|
|
52
|
+
"the file is accessible and is a valid YAML file.",
|
|
53
|
+
ex,
|
|
54
|
+
) from ex
|
|
55
|
+
|
|
56
|
+
self._config_file_path = path
|
|
57
|
+
self._validate()
|
|
58
|
+
|
|
59
|
+
def _validate(self):
|
|
60
|
+
if self._config is None:
|
|
61
|
+
raise InvalidApplicationConfigurationError(
|
|
62
|
+
f"Error reading application configuration: {self._config_file_path}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if "application" not in self._config:
|
|
66
|
+
raise InvalidApplicationConfigurationError(
|
|
67
|
+
"Application ('application') configuration cannot be found in "
|
|
68
|
+
f"{self._config_file_path}"
|
|
69
|
+
)
|
|
70
|
+
if "resources" not in self._config:
|
|
71
|
+
raise InvalidApplicationConfigurationError(
|
|
72
|
+
"Resources ('resources') configuration cannot be found in "
|
|
73
|
+
f"{self._config_file_path}"
|
|
74
|
+
)
|
|
75
|
+
if (
|
|
76
|
+
"title" not in self._config["application"]
|
|
77
|
+
or len(self._config["application"]["title"]) <= 0
|
|
78
|
+
):
|
|
79
|
+
raise InvalidApplicationConfigurationError(
|
|
80
|
+
"Application configuration key/value ('application>title') "
|
|
81
|
+
f"cannot be found or is empty in {self._config_file_path}"
|
|
82
|
+
)
|
|
83
|
+
self._application_object = self._config["application"]
|
|
84
|
+
self._resource_object = self._config["resources"]
|
|
85
|
+
|
|
86
|
+
def title(self) -> str:
|
|
87
|
+
return self._application_object["title"]
|
|
88
|
+
|
|
89
|
+
def pip_packages(self) -> Any:
|
|
90
|
+
if "pip-packages" in self._application_object:
|
|
91
|
+
return self._application_object["pip-packages"]
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def populate_app_manifest(
|
|
95
|
+
self, build_parameters: PackageBuildParameters
|
|
96
|
+
) -> ApplicationManifest:
|
|
97
|
+
application_manifest = ApplicationManifest()
|
|
98
|
+
application_manifest.api_version = DefaultValues.API_VERSION
|
|
99
|
+
application_manifest.command = build_parameters.command
|
|
100
|
+
|
|
101
|
+
application_manifest.environment = {}
|
|
102
|
+
application_manifest.environment[EnvironmentVariables.HOLOSCAN_APPLICATION] = (
|
|
103
|
+
str(build_parameters.app_dir)
|
|
104
|
+
)
|
|
105
|
+
application_manifest.environment[EnvironmentVariables.HOLOSCAN_INPUT_PATH] = (
|
|
106
|
+
str(build_parameters.input_dir)
|
|
107
|
+
)
|
|
108
|
+
application_manifest.environment[EnvironmentVariables.HOLOSCAN_OUTPUT_PATH] = (
|
|
109
|
+
str(build_parameters.output_dir)
|
|
110
|
+
)
|
|
111
|
+
application_manifest.environment[EnvironmentVariables.HOLOSCAN_WORKDIR] = str(
|
|
112
|
+
build_parameters.working_dir
|
|
113
|
+
)
|
|
114
|
+
application_manifest.environment[EnvironmentVariables.HOLOSCAN_MODEL_PATH] = (
|
|
115
|
+
str(build_parameters.models_dir)
|
|
116
|
+
)
|
|
117
|
+
application_manifest.environment[EnvironmentVariables.HOLOSCAN_CONFIG_PATH] = (
|
|
118
|
+
str(build_parameters.config_file_path)
|
|
119
|
+
)
|
|
120
|
+
application_manifest.environment[
|
|
121
|
+
EnvironmentVariables.HOLOSCAN_APP_MANIFEST_PATH
|
|
122
|
+
] = str(build_parameters.app_manifest_path)
|
|
123
|
+
application_manifest.environment[
|
|
124
|
+
EnvironmentVariables.HOLOSCAN_PKG_MANIFEST_PATH
|
|
125
|
+
] = str(build_parameters.package_manifest_path)
|
|
126
|
+
application_manifest.environment[EnvironmentVariables.HOLOSCAN_DOCS_PATH] = str(
|
|
127
|
+
build_parameters.docs_dir
|
|
128
|
+
)
|
|
129
|
+
application_manifest.environment[EnvironmentVariables.HOLOSCAN_LOGS_PATH] = str(
|
|
130
|
+
build_parameters.logs_dir
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
application_manifest.input = {
|
|
134
|
+
"path": build_parameters.input_dir,
|
|
135
|
+
"formats": self._application_object.get("input-formats", None),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
application_manifest.output = {
|
|
139
|
+
"path": build_parameters.output_dir,
|
|
140
|
+
"formats": self._application_object.get("output-formats", None),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
application_manifest.readiness = None
|
|
144
|
+
application_manifest.liveness = None
|
|
145
|
+
application_manifest.timeout = build_parameters.timeout
|
|
146
|
+
application_manifest.version = self._get_version(build_parameters)
|
|
147
|
+
application_manifest.working_directory = str(build_parameters.working_dir)
|
|
148
|
+
|
|
149
|
+
return application_manifest
|
|
150
|
+
|
|
151
|
+
def populate_package_manifest(
|
|
152
|
+
self, build_parameters: PackageBuildParameters
|
|
153
|
+
) -> PackageManifest:
|
|
154
|
+
package_manifest = PackageManifest()
|
|
155
|
+
package_manifest.api_version = DefaultValues.API_VERSION
|
|
156
|
+
package_manifest.application_root = str(build_parameters.app_dir)
|
|
157
|
+
package_manifest.model_root = str(build_parameters.models_dir)
|
|
158
|
+
|
|
159
|
+
package_manifest.models = {}
|
|
160
|
+
if build_parameters.models is not None:
|
|
161
|
+
for model in build_parameters.models:
|
|
162
|
+
package_manifest.models[model] = os.path.join(
|
|
163
|
+
package_manifest.model_root, model
|
|
164
|
+
)
|
|
165
|
+
package_manifest.resources = self._resource_object
|
|
166
|
+
package_manifest.version = self._get_version(build_parameters)
|
|
167
|
+
|
|
168
|
+
return package_manifest
|
|
169
|
+
|
|
170
|
+
def _get_version(self, build_parameters: PackageBuildParameters) -> str:
|
|
171
|
+
if build_parameters.version is None:
|
|
172
|
+
if "version" not in self._application_object:
|
|
173
|
+
raise InvalidApplicationConfigurationError(
|
|
174
|
+
"Application configuration key/value ('application>version') "
|
|
175
|
+
f"cannot be found or is empty in {self._config_file_path}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return self._application_object["version"]
|
|
179
|
+
|
|
180
|
+
return build_parameters.version
|
|
@@ -0,0 +1,426 @@
|
|
|
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 pprint
|
|
18
|
+
import shutil
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
|
23
|
+
|
|
24
|
+
from ..common.constants import Constants, DefaultValues
|
|
25
|
+
from ..common.dockerutils import (
|
|
26
|
+
build_docker_image,
|
|
27
|
+
create_and_get_builder,
|
|
28
|
+
docker_export_tarball,
|
|
29
|
+
)
|
|
30
|
+
from ..common.exceptions import WrongApplicationPathError
|
|
31
|
+
from .parameters import PackageBuildParameters, PlatformBuildResults, PlatformParameters
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BuilderBase:
|
|
35
|
+
"""
|
|
36
|
+
Docker container image builder base class.
|
|
37
|
+
Prepares files for building the docker image and calls Docker API to build the container image.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
build_parameters: PackageBuildParameters,
|
|
43
|
+
temp_dir: str,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Copy the application, model files, and user documentations here in __init__ since they
|
|
47
|
+
won't change when building different platforms.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
build_parameters (PackageBuildParameters): general build parameters
|
|
51
|
+
temp_dir (str): temporary directory to store files required for build
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
self._logger = logging.getLogger("packager.builder")
|
|
55
|
+
self._build_parameters = build_parameters
|
|
56
|
+
self._temp_dir = temp_dir
|
|
57
|
+
self._copy_application()
|
|
58
|
+
self._copy_model_files()
|
|
59
|
+
self._copy_docs()
|
|
60
|
+
self._copy_libs()
|
|
61
|
+
_ = self._write_dockerignore()
|
|
62
|
+
_ = self._copy_script()
|
|
63
|
+
|
|
64
|
+
def build(self, platform_parameters: PlatformParameters) -> PlatformBuildResults:
|
|
65
|
+
"""Build a new container image for a specific platform.
|
|
66
|
+
Copy supporting files, such as redistributables and generate Dockerfile for the build.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
platform_parameters (PlatformParameters): platform parameters
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
PlatformBuildResults: build results
|
|
73
|
+
"""
|
|
74
|
+
self._copy_supporting_files(platform_parameters)
|
|
75
|
+
docker_file_path = self._write_dockerfile(platform_parameters)
|
|
76
|
+
|
|
77
|
+
return self._build_internal(docker_file_path, platform_parameters)
|
|
78
|
+
|
|
79
|
+
def _build_internal(
|
|
80
|
+
self, dockerfile: str, platform_parameters: PlatformParameters
|
|
81
|
+
) -> PlatformBuildResults:
|
|
82
|
+
"""Prepare parameters for Docker buildx build
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
dockerfile (str): Path to Dockerfile to be built
|
|
86
|
+
platform_parameters (PlatformParameters): platform parameters
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
PlatformBuildResults: build results
|
|
90
|
+
"""
|
|
91
|
+
self.print_build_info(platform_parameters)
|
|
92
|
+
builder = create_and_get_builder(Constants.LOCAL_BUILDX_BUILDER_NAME)
|
|
93
|
+
|
|
94
|
+
build_result = PlatformBuildResults(platform_parameters)
|
|
95
|
+
|
|
96
|
+
cache_to = {"type": "local", "dest": self._build_parameters.build_cache}
|
|
97
|
+
cache_from = [{"type": "local", "src": self._build_parameters.build_cache}]
|
|
98
|
+
if platform_parameters.base_image is not None:
|
|
99
|
+
cache_from.append(
|
|
100
|
+
{"type": "registry", "ref": platform_parameters.base_image}
|
|
101
|
+
)
|
|
102
|
+
if platform_parameters.build_image is not None:
|
|
103
|
+
cache_from.append(
|
|
104
|
+
{"type": "registry", "ref": platform_parameters.build_image}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
builds = {
|
|
108
|
+
"builder": builder,
|
|
109
|
+
"cache": not self._build_parameters.no_cache,
|
|
110
|
+
"cache_from": None if self._build_parameters.no_cache else cache_from,
|
|
111
|
+
"cache_to": None if self._build_parameters.no_cache else cache_to,
|
|
112
|
+
"context_path": self._temp_dir,
|
|
113
|
+
"file": dockerfile,
|
|
114
|
+
"platforms": [platform_parameters.docker_arch],
|
|
115
|
+
"progress": "plain" if self._logger.root.level == logging.DEBUG else "auto",
|
|
116
|
+
"pull": True,
|
|
117
|
+
"tags": [platform_parameters.tag],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export_to_tar_ball = False
|
|
121
|
+
if self._build_parameters.tarball_output is not None:
|
|
122
|
+
build_result.tarball_filenaem = str(
|
|
123
|
+
self._build_parameters.tarball_output
|
|
124
|
+
/ f"{platform_parameters.tag}{Constants.TARBALL_FILE_EXTENSION}"
|
|
125
|
+
).replace(":", "-")
|
|
126
|
+
|
|
127
|
+
# Make result image available on 'docker image' only if arch matches
|
|
128
|
+
if platform_parameters.same_arch_as_system:
|
|
129
|
+
builds["load"] = True
|
|
130
|
+
build_result.docker_tag = platform_parameters.tag
|
|
131
|
+
export_to_tar_ball = self._build_parameters.tarball_output is not None
|
|
132
|
+
else:
|
|
133
|
+
if self._build_parameters.tarball_output is not None:
|
|
134
|
+
builds["output"] = {
|
|
135
|
+
# type=oci cannot be loaded by docker: https://github.com/docker/buildx/issues/59
|
|
136
|
+
"type": "docker",
|
|
137
|
+
"dest": build_result.tarball_filenaem,
|
|
138
|
+
}
|
|
139
|
+
else:
|
|
140
|
+
build_result.succeeded = False
|
|
141
|
+
build_result.error = (
|
|
142
|
+
"Skipped due to incompatible system architecture. "
|
|
143
|
+
"Use '--output' to write image to disk."
|
|
144
|
+
)
|
|
145
|
+
return build_result
|
|
146
|
+
|
|
147
|
+
builds["build_args"] = {
|
|
148
|
+
"UID": self._build_parameters.uid,
|
|
149
|
+
"GID": self._build_parameters.gid,
|
|
150
|
+
"UNAME": self._build_parameters.username,
|
|
151
|
+
"GPU_TYPE": platform_parameters.platform_config.value,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
self._logger.debug(
|
|
155
|
+
f"Building Holoscan Application Package: tag={platform_parameters.tag}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
build_docker_image(**builds)
|
|
160
|
+
build_result.succeeded = True
|
|
161
|
+
if export_to_tar_ball:
|
|
162
|
+
try:
|
|
163
|
+
self._logger.info(
|
|
164
|
+
f"Saving {platform_parameters.tag} to {build_result.tarball_filenaem}..."
|
|
165
|
+
)
|
|
166
|
+
docker_export_tarball(
|
|
167
|
+
build_result.tarball_filenaem, platform_parameters.tag
|
|
168
|
+
)
|
|
169
|
+
except Exception as ex:
|
|
170
|
+
build_result.error = f"Error saving tarball: {ex}"
|
|
171
|
+
build_result.succeeded = False
|
|
172
|
+
except Exception:
|
|
173
|
+
build_result.succeeded = False
|
|
174
|
+
build_result.error = (
|
|
175
|
+
"Error building image: see Docker output for additional details."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return build_result
|
|
179
|
+
|
|
180
|
+
def print_build_info(self, platform_parameters):
|
|
181
|
+
"""Print build information for the platform."""
|
|
182
|
+
self._logger.info(
|
|
183
|
+
f"""
|
|
184
|
+
===============================================================================
|
|
185
|
+
Building image for: {platform_parameters.platform.value}
|
|
186
|
+
Architecture: {platform_parameters.platform_arch.value}
|
|
187
|
+
Base Image: {platform_parameters.base_image}
|
|
188
|
+
Build Image: {platform_parameters.build_image if platform_parameters.build_image is not None else "N/A"}
|
|
189
|
+
Cache: {'Disabled' if self._build_parameters.no_cache else 'Enabled'}
|
|
190
|
+
Configuration: {platform_parameters.platform_config.value}
|
|
191
|
+
Holoscan SDK Package: {platform_parameters.holoscan_sdk_file if platform_parameters.holoscan_sdk_file is not None else "N/A"}
|
|
192
|
+
MONAI Deploy App SDK Package: {platform_parameters.monai_deploy_sdk_file if platform_parameters.monai_deploy_sdk_file is not None else "N/A"}
|
|
193
|
+
gRPC Health Probe: {platform_parameters.health_probe if platform_parameters.health_probe is not None else "N/A"}
|
|
194
|
+
SDK Version: {self._build_parameters.holoscan_sdk_version}
|
|
195
|
+
SDK: {self._build_parameters.sdk.value}
|
|
196
|
+
Tag: {platform_parameters.tag}
|
|
197
|
+
Included features/dependencies: {", ".join(self._build_parameters.includes) if self._build_parameters.includes else "N/A"}
|
|
198
|
+
""" # noqa: E501
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def _write_dockerignore(self):
|
|
202
|
+
"""Copy .dockerignore file to temporary location."""
|
|
203
|
+
# Write out .dockerignore file
|
|
204
|
+
dockerignore_source_file_path = (
|
|
205
|
+
Path(__file__).parent / "templates" / "dockerignore"
|
|
206
|
+
)
|
|
207
|
+
dockerignore_dest_file_path = os.path.join(self._temp_dir, ".dockerignore")
|
|
208
|
+
shutil.copyfile(dockerignore_source_file_path, dockerignore_dest_file_path)
|
|
209
|
+
return dockerignore_dest_file_path
|
|
210
|
+
|
|
211
|
+
def _copy_script(self):
|
|
212
|
+
"""Copy HAP/MAP tools.sh script to temporary directory"""
|
|
213
|
+
# Copy the tools script
|
|
214
|
+
tools_script_file_path = Path(__file__).parent / "templates" / "tools.sh"
|
|
215
|
+
tools_script_dest_file_path = os.path.join(self._temp_dir, "tools")
|
|
216
|
+
shutil.copyfile(tools_script_file_path, tools_script_dest_file_path)
|
|
217
|
+
return tools_script_dest_file_path
|
|
218
|
+
|
|
219
|
+
def _write_dockerfile(self, platform_parameters: PlatformParameters):
|
|
220
|
+
"""Write Dockerfile temporary location"""
|
|
221
|
+
docker_template_string = self._get_template(platform_parameters)
|
|
222
|
+
self._logger.debug(
|
|
223
|
+
f"""
|
|
224
|
+
========== Begin Dockerfile ==========
|
|
225
|
+
{docker_template_string}
|
|
226
|
+
=========== End Dockerfile ===========
|
|
227
|
+
"""
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
docker_file_path = os.path.join(self._temp_dir, DefaultValues.DOCKER_FILE_NAME)
|
|
231
|
+
with open(docker_file_path, "w") as docker_file:
|
|
232
|
+
docker_file.write(docker_template_string)
|
|
233
|
+
|
|
234
|
+
return os.path.abspath(docker_file_path)
|
|
235
|
+
|
|
236
|
+
def _copy_application(self):
|
|
237
|
+
"""Copy application to temporary location"""
|
|
238
|
+
# Copy application files to temp directory (under 'app' folder)
|
|
239
|
+
target_application_path = Path(os.path.join(self._temp_dir, "app"))
|
|
240
|
+
if os.path.exists(target_application_path):
|
|
241
|
+
shutil.rmtree(target_application_path)
|
|
242
|
+
|
|
243
|
+
if not os.path.exists(self._build_parameters.application):
|
|
244
|
+
raise WrongApplicationPathError(
|
|
245
|
+
f'Directory "{self._build_parameters.application}" not found.'
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if os.path.isfile(self._build_parameters.application):
|
|
249
|
+
shutil.copytree(
|
|
250
|
+
self._build_parameters.application.parent, target_application_path
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
shutil.copytree(self._build_parameters.application, target_application_path)
|
|
254
|
+
|
|
255
|
+
target_config_file_path = Path(os.path.join(self._temp_dir, "app.config"))
|
|
256
|
+
shutil.copyfile(
|
|
257
|
+
self._build_parameters.app_config_file_path, target_config_file_path
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def _copy_libs(self):
|
|
261
|
+
"""
|
|
262
|
+
- Copy additional libraries to the temporary application directory.
|
|
263
|
+
- Stores all subdirectories from the copied libraries to the 'additional_lib_paths'
|
|
264
|
+
parameter that will be used to set the LD_LIBRARY_PATH or PYTHONPATH environment variable
|
|
265
|
+
in the Dockerfile.
|
|
266
|
+
"""
|
|
267
|
+
if self._build_parameters.additional_libs is None:
|
|
268
|
+
return
|
|
269
|
+
target_libs_path = Path(os.path.join(self._temp_dir, "lib"))
|
|
270
|
+
if os.path.exists(target_libs_path):
|
|
271
|
+
shutil.rmtree(target_libs_path)
|
|
272
|
+
|
|
273
|
+
for lib_path in self._build_parameters.additional_libs:
|
|
274
|
+
self._logger.debug(
|
|
275
|
+
f"Copying additional libraries from {lib_path} to {target_libs_path}"
|
|
276
|
+
)
|
|
277
|
+
shutil.copytree(lib_path, target_libs_path, dirs_exist_ok=True)
|
|
278
|
+
|
|
279
|
+
subdirectories = [
|
|
280
|
+
os.path.join(
|
|
281
|
+
DefaultValues.HOLOSCAN_LIB_DIR,
|
|
282
|
+
os.path.join(root, subdir)
|
|
283
|
+
.replace(str(target_libs_path), "")
|
|
284
|
+
.lstrip("/"),
|
|
285
|
+
)
|
|
286
|
+
for root, dirs, _ in os.walk(target_libs_path)
|
|
287
|
+
for subdir in dirs
|
|
288
|
+
]
|
|
289
|
+
self._build_parameters.additional_lib_paths = ":".join(subdirectories)
|
|
290
|
+
|
|
291
|
+
def _copy_model_files(self):
|
|
292
|
+
"""Copy models to temporary location"""
|
|
293
|
+
if self._build_parameters.models:
|
|
294
|
+
target_models_root_path = os.path.join(self._temp_dir, "models")
|
|
295
|
+
os.makedirs(target_models_root_path, exist_ok=True)
|
|
296
|
+
|
|
297
|
+
for model in self._build_parameters.models:
|
|
298
|
+
target_model_path = os.path.join(target_models_root_path, model)
|
|
299
|
+
if self._build_parameters.models[model].is_dir():
|
|
300
|
+
shutil.copytree(
|
|
301
|
+
self._build_parameters.models[model], target_model_path
|
|
302
|
+
)
|
|
303
|
+
elif self._build_parameters.models[model].is_file():
|
|
304
|
+
os.makedirs(target_model_path, exist_ok=True)
|
|
305
|
+
target_model_path = os.path.join(
|
|
306
|
+
target_model_path, self._build_parameters.models[model].name
|
|
307
|
+
)
|
|
308
|
+
shutil.copy(self._build_parameters.models[model], target_model_path)
|
|
309
|
+
|
|
310
|
+
def _copy_docs(self):
|
|
311
|
+
"""Copy user documentations to temporary location"""
|
|
312
|
+
if self._build_parameters.docs is not None:
|
|
313
|
+
target_path = os.path.join(self._temp_dir, "docs")
|
|
314
|
+
shutil.copytree(self._build_parameters.docs, target_path)
|
|
315
|
+
|
|
316
|
+
def _get_template(self, platform_parameters: PlatformParameters):
|
|
317
|
+
"""Generate Dockerfile using Jinja2 engine"""
|
|
318
|
+
jinja_env = Environment(
|
|
319
|
+
loader=FileSystemLoader(Path(__file__).parent / "templates"),
|
|
320
|
+
undefined=StrictUndefined,
|
|
321
|
+
trim_blocks=True,
|
|
322
|
+
lstrip_blocks=True,
|
|
323
|
+
autoescape=True,
|
|
324
|
+
)
|
|
325
|
+
self._logger.debug(
|
|
326
|
+
f"""
|
|
327
|
+
========== Begin Build Parameters ==========
|
|
328
|
+
{pprint.pformat(self._build_parameters.to_jinja)}
|
|
329
|
+
=========== End Build Parameters ===========
|
|
330
|
+
"""
|
|
331
|
+
)
|
|
332
|
+
self._logger.debug(
|
|
333
|
+
f"""
|
|
334
|
+
========== Begin Platform Parameters ==========
|
|
335
|
+
{pprint.pformat(platform_parameters.to_jinja)}
|
|
336
|
+
=========== End Platform Parameters ===========
|
|
337
|
+
"""
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
jinja_template = jinja_env.get_template("Dockerfile.jinja2")
|
|
341
|
+
return jinja_template.render(
|
|
342
|
+
{
|
|
343
|
+
**self._build_parameters.to_jinja,
|
|
344
|
+
**platform_parameters.to_jinja,
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def _copy_supporting_files(self, platform_parameters: PlatformParameters):
|
|
349
|
+
"""Abstract base function to copy supporting files"""
|
|
350
|
+
return NotImplemented
|
|
351
|
+
|
|
352
|
+
def __init_subclass__(cls):
|
|
353
|
+
if cls._copy_supporting_files is BuilderBase._copy_supporting_files:
|
|
354
|
+
raise NotImplementedError(
|
|
355
|
+
"{cls} has not overwritten method {_copy_supporting_files}!"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class PythonAppBuilder(BuilderBase):
|
|
360
|
+
"""A subclass of BuilderBase for Python-based applications.
|
|
361
|
+
Copioes PyPI package and requirement.txt file
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
def __init__(
|
|
365
|
+
self,
|
|
366
|
+
build_parameters: PackageBuildParameters,
|
|
367
|
+
temp_dir: str,
|
|
368
|
+
) -> None:
|
|
369
|
+
BuilderBase.__init__(self, build_parameters, temp_dir)
|
|
370
|
+
|
|
371
|
+
def _copy_supporting_files(self, platform_parameters: PlatformParameters):
|
|
372
|
+
self._copy_sdk_file(platform_parameters.holoscan_sdk_file)
|
|
373
|
+
self._copy_sdk_file(platform_parameters.monai_deploy_sdk_file)
|
|
374
|
+
self._copy_pip_requirements()
|
|
375
|
+
|
|
376
|
+
def _copy_pip_requirements(self):
|
|
377
|
+
pip_folder = os.path.join(self._temp_dir, "pip")
|
|
378
|
+
os.makedirs(pip_folder, exist_ok=True)
|
|
379
|
+
pip_requirements_path = os.path.join(pip_folder, "requirements.txt")
|
|
380
|
+
with open(pip_requirements_path, "w") as requirements_file:
|
|
381
|
+
# Use local requirements.txt packages if provided, otherwise use sdk provided packages
|
|
382
|
+
if self._build_parameters.requirements_file_path is not None:
|
|
383
|
+
with open(self._build_parameters.requirements_file_path) as lr:
|
|
384
|
+
for line in lr:
|
|
385
|
+
requirements_file.write(line)
|
|
386
|
+
requirements_file.writelines("\n")
|
|
387
|
+
|
|
388
|
+
if self._build_parameters.pip_packages:
|
|
389
|
+
requirements_file.writelines(
|
|
390
|
+
"\n".join(self._build_parameters.pip_packages)
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def _copy_sdk_file(self, sdk_file: Optional[Path]):
|
|
394
|
+
if sdk_file is not None and os.path.isfile(sdk_file):
|
|
395
|
+
dest = os.path.join(self._temp_dir, sdk_file.name)
|
|
396
|
+
if os.path.exists(dest):
|
|
397
|
+
os.remove(dest)
|
|
398
|
+
shutil.copyfile(sdk_file, dest)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class CppAppBuilder(BuilderBase):
|
|
402
|
+
"""A subclass of BuilderBase for C++ applications.
|
|
403
|
+
Copies Debian.
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
def __init__(
|
|
407
|
+
self,
|
|
408
|
+
build_parameters: PackageBuildParameters,
|
|
409
|
+
temp_dir: str,
|
|
410
|
+
) -> None:
|
|
411
|
+
BuilderBase.__init__(self, build_parameters, temp_dir)
|
|
412
|
+
|
|
413
|
+
def _copy_supporting_files(self, platform_parameters: PlatformParameters):
|
|
414
|
+
"""Copies the SDK file to the temporary directory"""
|
|
415
|
+
if platform_parameters.holoscan_sdk_file is not None and os.path.isfile(
|
|
416
|
+
platform_parameters.holoscan_sdk_file
|
|
417
|
+
):
|
|
418
|
+
dest = os.path.join(
|
|
419
|
+
self._temp_dir, platform_parameters.holoscan_sdk_file.name
|
|
420
|
+
)
|
|
421
|
+
if os.path.exists(dest):
|
|
422
|
+
os.remove(dest)
|
|
423
|
+
shutil.copyfile(
|
|
424
|
+
platform_parameters.holoscan_sdk_file,
|
|
425
|
+
dest,
|
|
426
|
+
)
|