genesis-devtools 0.0.4__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.
File without changes
@@ -0,0 +1,17 @@
1
+ # Copyright 2025 Genesis Corporation.
2
+ #
3
+ # All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License. You may obtain
7
+ # a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ # License for the specific language governing permissions and limitations
15
+ # under the License.
16
+
17
+ from genesis_devtools.builder import dependency
@@ -0,0 +1,196 @@
1
+ # Copyright 2025 Genesis Corporation.
2
+ #
3
+ # All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License. You may obtain
7
+ # a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ # License for the specific language governing permissions and limitations
15
+ # under the License.
16
+ from __future__ import annotations
17
+
18
+ import abc
19
+ import os
20
+ import typing as tp
21
+
22
+ from genesis_devtools import constants as c
23
+
24
+
25
+ class Image(tp.NamedTuple):
26
+ """Image representation."""
27
+
28
+ script: str
29
+ profile: c.ImageProfileType = "ubuntu_24"
30
+ format: c.ImageFormatType = "raw"
31
+ name: str | None = None
32
+ envs: tp.List[str] | None = None
33
+ override: tp.Dict[str, tp.Any] | None = None
34
+
35
+ @classmethod
36
+ def from_config(
37
+ cls, image_config: tp.Dict[str, tp.Any], work_dir: str
38
+ ) -> "Image":
39
+ """Create an image from configuration."""
40
+ script = image_config.pop("script")
41
+ if not os.path.isabs(script):
42
+ script = os.path.join(work_dir, script)
43
+ return cls(script=script, **image_config)
44
+
45
+
46
+ class Element(tp.NamedTuple):
47
+ """Element representation."""
48
+
49
+ manifest: tp.Optional[str] = None
50
+ images: tp.Optional[tp.List[Image]] = None
51
+ artifacts: tp.Optional[tp.List[str]] = None
52
+
53
+ def __str__(self):
54
+ if self.manifest:
55
+ # TODO: Add implementation where manifest is used.
56
+ return "<Element manifest=...>"
57
+
58
+ if self.images and len(self.images) > 0:
59
+ name = ", ".join([f"{i.profile}" for i in self.images])
60
+ return f"<Element images={name}>"
61
+
62
+ return f"<Element {str(self)}>"
63
+
64
+ @classmethod
65
+ def from_config(
66
+ cls, element_config: tp.Dict[str, tp.Any], work_dir: str
67
+ ) -> "Element":
68
+ """Create an element from configuration."""
69
+ image_configs = element_config.pop("images", [])
70
+ images = [Image.from_config(img, work_dir) for img in image_configs]
71
+ return cls(images=images, **element_config)
72
+
73
+
74
+ class AbstractDependency(abc.ABC):
75
+ """Abstract dependency item.
76
+
77
+ This class defines the interface for a dependency item.
78
+ """
79
+
80
+ dependencies_store: tp.List["AbstractDependency"] = []
81
+
82
+ def __init_subclass__(cls, **kwargs) -> None:
83
+ super().__init_subclass__()
84
+ cls.dependencies_store.append(cls)
85
+
86
+ @abc.abstractproperty
87
+ def img_dest(self) -> tp.Optional[str]:
88
+ """Destination for the image."""
89
+
90
+ @property
91
+ def local_path(self) -> tp.Optional[str]:
92
+ """Local path to the dependency."""
93
+ return None
94
+
95
+ @abc.abstractmethod
96
+ def fetch(self, output_dir: str) -> None:
97
+ """Fetch the dependency."""
98
+
99
+ @abc.abstractclassmethod
100
+ def from_config(
101
+ cls, dep_config: tp.Dict[str, tp.Any], work_dir: str
102
+ ) -> "AbstractDependency":
103
+ """Create a dependency item from configuration."""
104
+
105
+ @classmethod
106
+ def find_dependency(
107
+ cls, dep_config: tp.Dict[str, tp.Any], work_dir: str
108
+ ) -> tp.Optional["AbstractDependency"]:
109
+ """Probe all dependencies to find the right one."""
110
+ for dep in cls.dependencies_store:
111
+ try:
112
+ return dep.from_config(dep_config, work_dir)
113
+ except Exception:
114
+ pass
115
+
116
+ return None
117
+
118
+
119
+ class AbstractImageBuilder(abc.ABC):
120
+ """Abstract image builder.
121
+
122
+ This class defines the interface for building images.
123
+ """
124
+
125
+ @abc.abstractmethod
126
+ def pre_build(
127
+ self,
128
+ image_dir: str,
129
+ image: Image,
130
+ deps: tp.List[AbstractDependency],
131
+ developer_keys: tp.Optional[str] = None,
132
+ ) -> None:
133
+ """Actions to prepare the environment for building the image."""
134
+
135
+ @abc.abstractmethod
136
+ def build(
137
+ self,
138
+ image_dir: str,
139
+ image: Image,
140
+ developer_keys: tp.Optional[str] = None,
141
+ ) -> None:
142
+ """Actions to build the image."""
143
+
144
+ @abc.abstractmethod
145
+ def post_build(
146
+ self,
147
+ image_dir: str,
148
+ image: Image,
149
+ ) -> None:
150
+ """Actions to perform after building the image."""
151
+
152
+ def run(
153
+ self,
154
+ image_dir: str,
155
+ image: Image,
156
+ deps: tp.List[AbstractDependency],
157
+ developer_keys: tp.Optional[str] = None,
158
+ ) -> None:
159
+ """Run the image builder."""
160
+ self.pre_build(image_dir, image, deps, developer_keys)
161
+ self.build(image_dir, image, developer_keys)
162
+ self.post_build(image_dir, image)
163
+
164
+
165
+ class DummyImageBuilder(AbstractImageBuilder):
166
+ """Dummy image builder.
167
+
168
+ Dummy builder that does nothing.
169
+ """
170
+
171
+ def pre_build(
172
+ self,
173
+ image_dir: str,
174
+ image: Image,
175
+ deps: tp.List[AbstractDependency],
176
+ developer_keys: tp.Optional[str] = None,
177
+ ) -> None:
178
+ """Actions to prepare the environment for building the image."""
179
+ return None
180
+
181
+ def build(
182
+ self,
183
+ image_dir: str,
184
+ image: Image,
185
+ developer_keys: tp.Optional[str] = None,
186
+ ) -> None:
187
+ """Actions to build the image."""
188
+ return None
189
+
190
+ def post_build(
191
+ self,
192
+ image_dir: str,
193
+ image: Image,
194
+ ) -> None:
195
+ """Actions to perform after building the image."""
196
+ return None
@@ -0,0 +1,108 @@
1
+ # Copyright 2025 Genesis Corporation.
2
+ #
3
+ # All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License. You may obtain
7
+ # a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ # License for the specific language governing permissions and limitations
15
+ # under the License.
16
+
17
+ import typing as tp
18
+ import tempfile
19
+
20
+ from genesis_devtools.builder import base
21
+ from genesis_devtools.logger import AbstractLogger, DummyLogger
22
+
23
+
24
+ class SimpleBuilder:
25
+ """Abstract element builder.
26
+
27
+ This class defines the interface for building elements.
28
+ """
29
+
30
+ DEP_KEY = "deps"
31
+ ELEMENT_KEY = "elements"
32
+
33
+ def __init__(
34
+ self,
35
+ work_dir: str,
36
+ deps: tp.List[base.AbstractDependency],
37
+ elements: tp.List[base.Element],
38
+ image_builder: base.AbstractImageBuilder,
39
+ logger: tp.Optional[AbstractLogger] = None,
40
+ ) -> None:
41
+ super().__init__()
42
+ self._deps = deps
43
+ self._elements = elements
44
+ self._image_builder = image_builder
45
+ self._work_dir = work_dir
46
+ self._logger = logger or DummyLogger()
47
+
48
+ def fetch_dependency(self, deps_dir: str) -> None:
49
+ """Fetch common dependencies for elements."""
50
+ self._logger.important("Fetching dependencies")
51
+ for dep in self._deps:
52
+ self._logger.info(f"Fetching dependency: {dep}")
53
+ dep.fetch(deps_dir)
54
+
55
+ def build(
56
+ self,
57
+ build_dir: tp.Optional[str] = None,
58
+ developer_keys: tp.Optional[str] = None,
59
+ ) -> None:
60
+ """Build all elements."""
61
+ self._logger.important("Building elements")
62
+ for e in self._elements:
63
+ self._logger.info(f"Building element: {e}")
64
+ for img in e.images:
65
+ # The build_dir is used only for debugging purposes to observe
66
+ # the content of the image. In production, the image is built
67
+ # in a temporary directory.
68
+ if build_dir is not None:
69
+ self._image_builder.run(
70
+ build_dir, img, self._deps, developer_keys
71
+ )
72
+ else:
73
+ with tempfile.TemporaryDirectory() as temp_dir:
74
+ self._image_builder.run(
75
+ temp_dir, img, self._deps, developer_keys
76
+ )
77
+
78
+ @classmethod
79
+ def from_config(
80
+ cls,
81
+ work_dir: str,
82
+ build_config: tp.Dict[str, tp.Any],
83
+ image_builder: base.AbstractImageBuilder,
84
+ logger: tp.Optional[AbstractLogger] = None,
85
+ ) -> "SimpleBuilder":
86
+ """Create a builder from configuration."""
87
+ # Prepare dependencies entries but do not fetch them
88
+ deps = []
89
+ dep_configs = build_config.get(cls.DEP_KEY, [])
90
+ for dep in dep_configs:
91
+ dep_item = base.AbstractDependency.find_dependency(dep, work_dir)
92
+ if dep_item is None:
93
+ raise ValueError(
94
+ f"Unable to handle dependency: {dep}. Unknown type."
95
+ )
96
+ deps.append(dep_item)
97
+
98
+ # Prepare elements
99
+ element_configs = build_config.get(cls.ELEMENT_KEY, [])
100
+ elements = [
101
+ base.Element.from_config(elem, work_dir)
102
+ for elem in element_configs
103
+ ]
104
+
105
+ if not elements:
106
+ raise ValueError("No elements found in configuration")
107
+
108
+ return cls(work_dir, deps, elements, image_builder, logger)
@@ -0,0 +1,125 @@
1
+ # Copyright 2025 Genesis Corporation.
2
+ #
3
+ # All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License. You may obtain
7
+ # a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ # License for the specific language governing permissions and limitations
15
+ # under the License.
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import shutil
20
+ import typing as tp
21
+
22
+ import bazooka
23
+
24
+ from genesis_devtools.builder import base
25
+
26
+
27
+ class LocalPathDependency(base.AbstractDependency):
28
+ """Local path dependency item."""
29
+
30
+ def __init__(self, path: str, img_dest: str) -> None:
31
+ super().__init__()
32
+ self._path = path
33
+ self._img_dest = img_dest
34
+ self._local_path = None
35
+
36
+ @property
37
+ def img_dest(self) -> str | None:
38
+ """Destination for the image."""
39
+ return self._img_dest
40
+
41
+ @property
42
+ def local_path(self) -> str | None:
43
+ """Local path to the dependency."""
44
+ return self._local_path
45
+
46
+ def fetch(self, output_dir: str) -> None:
47
+ """Fetch the dependency."""
48
+ path = self._path
49
+ if os.path.isdir(path):
50
+ name = os.path.basename(path)
51
+ shutil.copytree(path, os.path.join(output_dir, name))
52
+ self._local_path = os.path.join(output_dir, name)
53
+ else:
54
+ shutil.copy(path, output_dir)
55
+ self._local_path = os.path.join(output_dir, os.path.basename(path))
56
+
57
+ def __str__(self):
58
+ return f"Local path -> {self._path}"
59
+
60
+ @classmethod
61
+ def from_config(
62
+ cls, dep_config: tp.Dict[str, tp.Any], work_dir: str
63
+ ) -> "LocalPathDependency":
64
+ """Create a dependency item from configuration."""
65
+ if "path" not in dep_config or "src" not in dep_config["path"]:
66
+ raise ValueError("Path not found in dependency configuration")
67
+
68
+ path = dep_config["path"]["src"]
69
+ img_dest = dep_config["dst"]
70
+
71
+ if not os.path.isabs(path):
72
+ path = os.path.join(work_dir, path)
73
+
74
+ return cls(path, img_dest)
75
+
76
+
77
+ class HttpDependency(base.AbstractDependency):
78
+ """HTTP dependency item."""
79
+
80
+ CHUNK_SIZE = 8192
81
+
82
+ def __init__(self, endpoint: str, img_dest: str) -> None:
83
+ super().__init__()
84
+ self._endpoint = endpoint
85
+ self._img_dest = img_dest
86
+ self._local_path = None
87
+
88
+ @property
89
+ def img_dest(self) -> str | None:
90
+ """Destination for the image."""
91
+ return self._img_dest
92
+
93
+ @property
94
+ def local_path(self) -> str | None:
95
+ """Local path to the dependency."""
96
+ return self._local_path
97
+
98
+ def fetch(self, output_dir: str) -> None:
99
+ """Fetch the dependency."""
100
+ filename = os.path.basename(self._endpoint)
101
+ output_path = os.path.join(output_dir, filename)
102
+
103
+ with bazooka.get(self._endpoint, stream=True) as r:
104
+ r.raise_for_status()
105
+ with open(output_path, "wb") as f:
106
+ for chunk in r.iter_content(chunk_size=self.CHUNK_SIZE):
107
+ f.write(chunk)
108
+
109
+ self._local_path = output_path
110
+
111
+ def __str__(self):
112
+ return f"URL -> {self._endpoint}"
113
+
114
+ @classmethod
115
+ def from_config(
116
+ cls, dep_config: tp.Dict[str, tp.Any], work_dir: str
117
+ ) -> "LocalPathDependency":
118
+ """Create a dependency item from configuration."""
119
+ if "http" not in dep_config or "src" not in dep_config["http"]:
120
+ raise ValueError("URL not found in dependency configuration")
121
+
122
+ endpoint = dep_config["http"]["src"]
123
+ img_dest = dep_config["dst"]
124
+
125
+ return cls(endpoint, img_dest)
@@ -0,0 +1,201 @@
1
+ # Copyright 2025 Genesis Corporation.
2
+ #
3
+ # All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License. You may obtain
7
+ # a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ # License for the specific language governing permissions and limitations
15
+ # under the License.
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import subprocess
20
+ import typing as tp
21
+ import shutil
22
+ from importlib.resources import files
23
+
24
+ from genesis_devtools.builder import base
25
+ from genesis_devtools.logger import AbstractLogger, DummyLogger
26
+ from genesis_devtools import constants as c
27
+
28
+
29
+ file_provisioner_tmpl = """
30
+ provisioner "file" {{
31
+ source = "{source}"
32
+ destination = "{tmp_destination}"
33
+ }}
34
+ provisioner "shell" {{
35
+ inline = [
36
+ "sudo mv {tmp_destination} {destination}",
37
+ ]
38
+ }}
39
+ """
40
+
41
+
42
+ dev_keys_provisioner_tmpl = """
43
+ provisioner "file" {{
44
+ source = "{source}"
45
+ destination = "/tmp/__dev_keys"
46
+ }}
47
+ """
48
+
49
+
50
+ packer_build_tmpl = """
51
+ variable "output_directory" {{
52
+ type = string
53
+ default = "{output_directory}"
54
+ }}
55
+
56
+ build {{
57
+ source "qemu.{profile}" {{
58
+ name = "{name}"
59
+ }}
60
+
61
+ {file_provisioners}
62
+
63
+ provisioner "shell" {{
64
+ execute_command = "sudo -S env {{{{ .Vars }}}} {{{{ .Path }}}}"
65
+ script = "{script}"
66
+ env = {{
67
+ EXAMPLE_VARIABLE = "example_value"
68
+ }}
69
+ }}
70
+
71
+ {developer_keys}
72
+ }}
73
+ """
74
+
75
+
76
+ class PackerVariable(tp.NamedTuple):
77
+ name: str
78
+ value: str | int | float | list | dict | None = ""
79
+ var_tmpl: str = "{name} = {value}"
80
+
81
+ def render(self) -> str:
82
+ data = self._asdict()
83
+ data.pop("var_tmpl", None)
84
+
85
+ # Need quotes for strings in HCL
86
+ if isinstance(self.value, str):
87
+ data["value"] = f'"{self.value}"'
88
+
89
+ return self.var_tmpl.format(**data)
90
+
91
+ @classmethod
92
+ def variable_file_content(cls, overrides: tp.Dict[str, tp.Any]) -> str:
93
+ if not overrides:
94
+ return ""
95
+
96
+ variables = []
97
+ for k, v in overrides.items():
98
+ variables.append(cls(name=k, value=v))
99
+
100
+ return "\n".join([v.render() for v in variables])
101
+
102
+
103
+ def _get_profile_files(base: str) -> str:
104
+ """Get base files for the image."""
105
+ profile_files = []
106
+ for bfile in files(f"{c.PKG_NAME}.packer.{base}").iterdir():
107
+ profile_files.append(bfile)
108
+
109
+ return profile_files
110
+
111
+
112
+ class PackerBuilder(base.DummyImageBuilder):
113
+ """Packer image builder.
114
+
115
+ Packer image builder that uses Packer tools to build images.
116
+ """
117
+
118
+ def __init__(
119
+ self, output_dir: str, logger: AbstractLogger | None = None
120
+ ) -> None:
121
+ super().__init__()
122
+ self._logger = logger or DummyLogger()
123
+ self._output_dir = output_dir
124
+
125
+ def pre_build(
126
+ self,
127
+ image_dir: str,
128
+ image: base.Image,
129
+ deps: tp.List[base.AbstractDependency],
130
+ developer_keys: str | None = None,
131
+ ) -> None:
132
+ """Actions to prepare the environment for building the image."""
133
+
134
+ # Prepare the packer build file
135
+ # Data provisioners
136
+ provisioners = []
137
+ for i, dep in enumerate(deps):
138
+ tmp_dest = os.path.join(
139
+ "/tmp/", os.path.basename(dep.img_dest) + f"_{i}"
140
+ )
141
+ provisioners.append(
142
+ file_provisioner_tmpl.format(
143
+ source=dep.local_path,
144
+ destination=dep.img_dest,
145
+ tmp_destination=tmp_dest,
146
+ )
147
+ )
148
+
149
+ # Prepare developer
150
+ if developer_keys:
151
+ dev_key_path = os.path.join(image_dir, "__dev_keys")
152
+ with open(dev_key_path, "w") as f:
153
+ f.write(developer_keys)
154
+ developer_keys_prov = dev_keys_provisioner_tmpl.format(
155
+ source=dev_key_path
156
+ )
157
+ else:
158
+ developer_keys_prov = ""
159
+
160
+ profile = image.profile.replace("_", "-")
161
+ packer_build = packer_build_tmpl.format(
162
+ profile=profile,
163
+ name=image.name or profile,
164
+ file_provisioners="\n".join(provisioners),
165
+ script=image.script,
166
+ developer_keys=developer_keys_prov,
167
+ output_directory=self._output_dir,
168
+ )
169
+
170
+ # Write the packer build file
171
+ main_path = os.path.join(image_dir, "main.pkr.hcl")
172
+ with open(main_path, "w") as f:
173
+ f.write(packer_build)
174
+
175
+ # Copy profile files to the image directory
176
+ profile_files = _get_profile_files(image.profile)
177
+ for bfile in profile_files:
178
+ shutil.copy(bfile, image_dir)
179
+
180
+ # Override variables if they are provided
181
+ if variables := PackerVariable.variable_file_content(
182
+ image.override or ()
183
+ ):
184
+ with open(
185
+ os.path.join(image_dir, "overrides.auto.pkrvars.hcl"), "w"
186
+ ) as f:
187
+ f.write(variables)
188
+
189
+ subprocess.run(["packer", "init", image_dir], check=True)
190
+
191
+ def build(
192
+ self,
193
+ image_dir: str,
194
+ image: base.Image,
195
+ developer_keys: str | None = None,
196
+ ) -> None:
197
+ """Actions to build the image."""
198
+ self._logger.important(f"Build image: {image.name}")
199
+ subprocess.run(
200
+ ["packer", "build", "-parallel-builds=1", image_dir], check=True
201
+ )
File without changes