Skip to content

Commit b3aa076

Browse files
Added distributable docker image generation (#10)
* Added distributable docker image generation + And a unit test! * fixed CLI error on test command * Doc changes, bugfixes * removed root user from the distributable dockerfile --------- Co-authored-by: tyler-g-hudson <[email protected]>
1 parent 9f8c3ae commit b3aa076

File tree

7 files changed

+236
-8
lines changed

7 files changed

+236
-8
lines changed

test/fixtures_isce3.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
cmake_config_dockerfile,
1414
cmake_install_dockerfile,
1515
)
16+
from wigwam._docker_distrib import distrib_dockerfile
1617
from wigwam._docker_git import git_extract_dockerfile
18+
from wigwam._utils import get_libdir
19+
from wigwam.defaults import install_prefix
1720
from wigwam.setup_commands import setup_all
1821

1922
from .utils import determine_scope, generate_tag, remove_docker_image
@@ -87,6 +90,20 @@ def isce3_env_dev_image_tag(
8790
raise ValueError("No development environment image tag found.")
8891

8992

93+
@fixture(scope=determine_scope)
94+
def isce3_env_runtime_image_tag(
95+
isce3_build_tag: str,
96+
isce3_setup_images: dict[str, Image],
97+
) -> str:
98+
"""Return the tag of the ISCE3 development environment image."""
99+
pattern = re.compile(rf".*{isce3_build_tag}.*mamba-runtime")
100+
for tag in isce3_setup_images:
101+
if pattern.match(tag):
102+
return tag
103+
104+
raise ValueError("No development environment image tag found.")
105+
106+
90107
@fixture(scope=determine_scope)
91108
def isce3_git_repo_tag() -> str:
92109
"""Return a tag for the ISCE3 repository image."""
@@ -191,3 +208,36 @@ def isce3_cmake_install_image(
191208
)
192209

193210
remove_docker_image(isce3_cmake_install_tag)
211+
212+
213+
@fixture(scope=determine_scope)
214+
def isce3_distributable_tag() -> str:
215+
"""Return a tag for the ISCE3 CMake install image."""
216+
return generate_tag("isce3-distributable")
217+
218+
219+
@fixture(scope=determine_scope)
220+
def isce3_distributable_image(
221+
isce3_distributable_tag: str,
222+
isce3_cmake_install_tag: str,
223+
isce3_env_runtime_image_tag: str,
224+
isce3_cmake_install_image: Image, # type: ignore
225+
) -> Iterator[Image]:
226+
"""Return the ISCE3 distributable image."""
227+
libdir = get_libdir(isce3_cmake_install_tag)
228+
229+
dockerfile = distrib_dockerfile(
230+
base=isce3_env_runtime_image_tag,
231+
source_tag=isce3_cmake_install_tag,
232+
source_path=install_prefix(),
233+
distrib_path=install_prefix(),
234+
libdir=libdir,
235+
)
236+
237+
yield Image.build(
238+
tag=isce3_distributable_tag,
239+
dockerfile_string=dockerfile,
240+
no_cache=False,
241+
)
242+
243+
remove_docker_image(isce3_distributable_tag)

test/test_docker_cmake.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,18 @@ def test_test_command(
308308
quiet_fail=False,
309309
)
310310
assert output_xml.is_file()
311+
312+
@mark.isce3
313+
@mark.slow
314+
def test_isce3_distributable_image(
315+
self,
316+
isce3_distributable_image: Image,
317+
):
318+
"""
319+
Test the ISCE3 Distributable image.
320+
321+
NOTE: This test runs very slowly because it requires the building of all
322+
ISCE3 base images.
323+
"""
324+
isce3_distributable_image.run(command='python -c "import isce3"')
325+
isce3_distributable_image.run(command='python -c "import nisar"')

wigwam/_docker_distrib.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from textwrap import dedent
5+
6+
7+
def distrib_dockerfile(
8+
base: str,
9+
source_tag: str,
10+
source_path: os.PathLike[str] | str,
11+
distrib_path: os.PathLike[str] | str,
12+
libdir: str,
13+
) -> str:
14+
"""
15+
Returns a Dockerfile for a distributable build.
16+
17+
The distributable image includes the installed software and all of its runtime
18+
dependencies, but excludes build-time dependencies. The working directory on the
19+
generated image will also be moved to the location given at `distrib_path`.
20+
21+
Parameters
22+
----------
23+
base : str
24+
The base image tag.
25+
source_tag : str
26+
The tag of the image on which the project is installed.
27+
source_path : os.PathLike[str] or str
28+
The installation path of the project on the source image.
29+
distrib_path : os.PathLike[str] or str
30+
The desired installation path on the distributable image.
31+
libdir : str
32+
The base name of the directory where object libraries are stored (e.g. "lib" or
33+
"lib64"). This should match the value of `LIBDIR` in CMake's `GNUInstallDirs`
34+
module.
35+
See https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html.
36+
37+
Returns
38+
-------
39+
dockerfile: str
40+
The generated Dockerfile.
41+
"""
42+
dockerfile: str = dedent(
43+
f"""
44+
FROM {source_tag} as source
45+
46+
FROM {base}
47+
48+
COPY --from=source {source_path} {distrib_path}
49+
50+
ENV LD_LIBRARY_PATH $LD_LIBRARY_PATH:{distrib_path}/{libdir}
51+
ENV PYTHONPATH $PYTHONPATH:{distrib_path}/packages
52+
53+
ENV ISCE3_PREFIX={distrib_path}
54+
WORKDIR $ISCE3_PREFIX
55+
"""
56+
).strip()
57+
58+
return dockerfile

wigwam/_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,24 @@ def test_image(image: Image, expression: str) -> bool:
203203
raise
204204

205205

206+
def get_libdir(base_tag: str) -> str:
207+
"""
208+
Determine if the given image uses `lib64` or `lib` as its `LIBDIR`.
209+
210+
This function assumes that the base_tag has something at either
211+
`$INSTALL_PREFIX/lib64` or `$INSTALL_PREFIX/lib`.
212+
"""
213+
with temp_image(base_tag) as temp_img:
214+
for libdir in ["lib64", "lib"]:
215+
if test_image(temp_img, f'"$INSTALL_PREFIX/{libdir}"'):
216+
return libdir
217+
else:
218+
raise ValueError(
219+
"could not find a directory named $INSTALL_PREFIX/lib64"
220+
" or $INSTALL_PREFIX/lib in the specified image"
221+
)
222+
223+
206224
def _package_manager_check(image: Image) -> PackageManager:
207225
"""
208226
Returns the package manager present on an image.

wigwam/cli/build_commands.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
configure_cmake,
99
copy_dir,
1010
get_archive,
11+
make_distrib,
1112
)
1213
from ._utils import add_tag_argument, help_formatter
1314

@@ -139,10 +140,47 @@ def init_build_parsers(subparsers: argparse._SubParsersAction) -> None:
139140
)
140141
add_tag_argument(parser=install_parser, default="installed")
141142

143+
distrib_parser = subparsers.add_parser(
144+
"make-distrib",
145+
parents=[no_cache_params],
146+
help="Creates a distributable image.",
147+
formatter_class=help_formatter,
148+
)
149+
distrib_parser.add_argument(
150+
"--tag",
151+
"-t",
152+
default="isce3",
153+
type=str,
154+
help="The complete tag of the Docker image to be created. " 'Default: "isce3"',
155+
)
156+
distrib_parser.add_argument(
157+
"--base",
158+
"-b",
159+
default="setup-mamba-runtime",
160+
type=str,
161+
help="The complete tag of the Docker image to be created. "
162+
'Default: "setup-mamba-runtime"',
163+
)
164+
distrib_parser.add_argument(
165+
"--source-tag",
166+
"-s",
167+
default="build-installed",
168+
type=str,
169+
help="The tag or ID of the source image which has the project installed. "
170+
' Defaults to "build-installed".',
171+
)
172+
142173

143174
def build_command_names() -> List[str]:
144175
"""Returns a list of all build command names."""
145-
return ["get-archive", "copydir", "cmake-config", "cmake-compile", "cmake-install"]
176+
return [
177+
"get-archive",
178+
"copydir",
179+
"cmake-config",
180+
"cmake-compile",
181+
"cmake-install",
182+
"make-distrib",
183+
]
146184

147185

148186
def run_build(args: argparse.Namespace, command: str) -> None:
@@ -156,3 +194,5 @@ def run_build(args: argparse.Namespace, command: str) -> None:
156194
compile_cmake(**vars(args))
157195
elif command == "cmake-install":
158196
cmake_install(**vars(args))
197+
elif command == "make-distrib":
198+
make_distrib(**vars(args))

wigwam/cli/util_commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def init_util_parsers(subparsers: argparse._SubParsersAction, prefix: str) -> No
4444
dropin_parser.add_argument(
4545
"tag", metavar="IMAGE_TAG", type=str, help="The tag or ID of the desired image."
4646
)
47-
test_parser.add_argument(
47+
dropin_parser.add_argument(
4848
"--default-user",
4949
action="store_true",
5050
help="Run as the default user on the image. If not used, will run as the "

wigwam/commands.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,20 @@
1313
cmake_config_dockerfile,
1414
cmake_install_dockerfile,
1515
)
16+
from ._docker_distrib import distrib_dockerfile
1617
from ._docker_git import git_extract_dockerfile
1718
from ._docker_insert import insert_dir_dockerfile
1819
from ._docker_mamba import mamba_lockfile_command
1920
from ._image import Image
2021
from ._url_reader import URLReader
21-
from ._utils import image_command_check, is_conda_pkg_name, prefix_image_tag, temp_image
22-
from .defaults import build_prefix
22+
from ._utils import (
23+
get_libdir,
24+
image_command_check,
25+
is_conda_pkg_name,
26+
prefix_image_tag,
27+
temp_image,
28+
)
29+
from .defaults import build_prefix, install_prefix
2330

2431

2532
def get_archive(
@@ -57,13 +64,13 @@ def get_archive(
5764
Image
5865
The generated image.
5966
"""
60-
if url_reader is None:
61-
with temp_image(base) as temp_img:
62-
_, url_reader, _ = image_command_check(temp_img)
63-
6467
img_tag = prefix_image_tag(tag)
6568
base_tag = prefix_image_tag(base)
6669

70+
if url_reader is None:
71+
with temp_image(base_tag) as temp_img:
72+
_, url_reader, _ = image_command_check(temp_img)
73+
6774
dockerfile = git_extract_dockerfile(
6875
base=base_tag,
6976
directory=directory,
@@ -256,6 +263,46 @@ def cmake_install(tag: str, base: str, no_cache: bool = False) -> Image:
256263
)
257264

258265

266+
def make_distrib(tag: str, base: str, source_tag: str, no_cache: bool = False) -> Image:
267+
"""
268+
Produces a distributable image.
269+
270+
Parameters
271+
----------
272+
tag : str
273+
The image tag.
274+
base : str
275+
The base image tag.
276+
source_tag : str
277+
The tag of the source image from which to acquire the install directory.
278+
no_cache : bool, optional
279+
Run Docker build with no cache if True. Defaults to False.
280+
281+
Returns
282+
-------
283+
Image
284+
The generated image.
285+
"""
286+
287+
prefixed_base_tag: str = prefix_image_tag(base)
288+
prefixed_source_tag: str = prefix_image_tag(source_tag)
289+
290+
# Unlike with the CMake Install function, `libdir` can be checked directly on this
291+
# image because it has something at $INSTALL_PREFIX. Check the base tag for lib64 or
292+
# lib.
293+
libdir: str = get_libdir(prefixed_base_tag)
294+
295+
dockerfile = distrib_dockerfile(
296+
base=prefixed_base_tag,
297+
source_tag=prefixed_source_tag,
298+
source_path=install_prefix(),
299+
distrib_path=install_prefix(),
300+
libdir=libdir,
301+
)
302+
303+
return Image.build(tag=tag, dockerfile_string=dockerfile, no_cache=no_cache)
304+
305+
259306
def test(
260307
tag: str,
261308
output_xml: os.PathLike[str] | str,

0 commit comments

Comments
 (0)