spl-core 7.12.1__tar.gz → 7.14.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.
- {spl_core-7.12.1 → spl_core-7.14.0}/PKG-INFO +1 -1
- {spl_core-7.12.1 → spl_core-7.14.0}/pyproject.toml +1 -1
- spl_core-7.14.0/src/spl_core/__init__.py +1 -0
- spl_core-7.14.0/src/spl_core/test_utils/artifacts_archiver.py +654 -0
- spl_core-7.12.1/src/spl_core/__init__.py +0 -1
- spl_core-7.12.1/src/spl_core/test_utils/artifacts_archiver.py +0 -318
- {spl_core-7.12.1 → spl_core-7.14.0}/LICENSE +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/README.md +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/__run.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/common/__init__.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/common/path.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/common.cmake +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/conan.cmake +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/config/KConfig +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/gcov_maid/__init__.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/gcov_maid/gcov_maid.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kconfig/__init__.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kconfig/kconfig.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kconfig.cmake +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/__init__.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/create.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/.vscode/cmake-variants.json +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/KConfig +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/src/greeter/CMakeLists.txt +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/src/greeter/doc/_images/screenshot.png +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/src/greeter/doc/index.md +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/src/greeter/src/greeter.c +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/src/greeter/src/greeter.h +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/src/greeter/test/test_greeter.cc +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/src/main/CMakeLists.txt +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/src/main/doc/index.md +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/src/main/src/main.c +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/test/EnglishVariant/test__EnglishVariant.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/test/German/test__GermanVariant.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/variants/EnglishVariant/config.cmake +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/variants/EnglishVariant/parts.cmake +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/variants/GermanVariant/config.cmake +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/variants/GermanVariant/config.txt +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/variants/GermanVariant/parts.cmake +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.gitignore +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.vscode/cmake-kits.json +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.vscode/extensions.json +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.vscode/launch.json +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.vscode/settings.json +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.vscode/tasks.json +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/CMakeLists.txt +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/README.md +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/bootstrap.json +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/build.bat +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/build.ps1 +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/conf.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/Doxyfile.in +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/common/index.md +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/components/index.md +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/doxygen-awesome/LICENSE +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/doxygen-awesome/doxygen-awesome.css +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/software_architecture/index.md +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/software_requirements/index.md +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/test_report_template.txt +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/index.md +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/pypeline.yaml +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/pyproject.toml +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/pytest.ini +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/scoopfile.json +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/tools/toolchains/clang/toolchain.cmake +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/tools/toolchains/gcc/toolchain.cmake +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/main.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/spl.cmake +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/steps/collect_pr_changes.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/test_utils/archive_artifacts_collection.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/test_utils/base_variant_test_runner.py +0 -0
- {spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/test_utils/spl_build.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "7.14.0"
|
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import py7zr
|
|
9
|
+
from py_app_dev.core.logging import logger
|
|
10
|
+
from py_app_dev.core.subprocess import SubprocessExecutor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class BuildMetadata:
|
|
15
|
+
"""
|
|
16
|
+
Contains build metadata extracted from environment variables.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
branch_name: The branch name, PR identifier (e.g., "PR-123"), or tag name
|
|
20
|
+
build_number: The build number or "local_build"
|
|
21
|
+
is_tag: Whether this is a tag build
|
|
22
|
+
pr_number: The PR number (without "PR-" prefix) for pull request builds, None otherwise
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
branch_name: str
|
|
26
|
+
build_number: str
|
|
27
|
+
is_tag: bool
|
|
28
|
+
pr_number: Optional[str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class GitMetadata:
|
|
33
|
+
"""
|
|
34
|
+
Contains git metadata extracted from environment variables or git commands.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
commit_id: The git commit SHA (full hash)
|
|
38
|
+
commit_message: The git commit message subject line (first line only)
|
|
39
|
+
repository_url: The git repository URL (from remote.origin.url)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
commit_id: Optional[str]
|
|
43
|
+
commit_message: Optional[str]
|
|
44
|
+
repository_url: Optional[str]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ArtifactsArchive:
|
|
48
|
+
"""
|
|
49
|
+
This class represents a single archive containing artifacts.
|
|
50
|
+
It collects artifacts to be packed and archived.
|
|
51
|
+
|
|
52
|
+
Currently supports 7z and Artifactory.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ArchiveArtifact:
|
|
57
|
+
"""
|
|
58
|
+
Represents a single artifact to be archived.
|
|
59
|
+
This class holds the archive path (relative to the output directory of the 7z archive)
|
|
60
|
+
and the absolute path of the artifact.
|
|
61
|
+
It is used to ensure that artifacts are correctly archived with their intended paths.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
archive_path: Path
|
|
65
|
+
absolute_path: Path
|
|
66
|
+
|
|
67
|
+
def __init__(self, out_dir: Path, archive_name: str) -> None:
|
|
68
|
+
self.out_dir: Path = out_dir
|
|
69
|
+
self.archive_name: str = archive_name
|
|
70
|
+
self.archive_artifacts: List[ArtifactsArchive.ArchiveArtifact] = []
|
|
71
|
+
|
|
72
|
+
def register(self, artifacts: List[Path]) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Register artifacts for archiving.
|
|
75
|
+
Args:
|
|
76
|
+
artifacts: List of paths to artifacts (files or directories) to be archived.
|
|
77
|
+
"""
|
|
78
|
+
for artifact in artifacts:
|
|
79
|
+
self._add_artifact(artifact)
|
|
80
|
+
|
|
81
|
+
def _add_artifact(self, artifact_path: Path) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Add an artifact (file or directory) to the archive list.
|
|
84
|
+
Args:
|
|
85
|
+
artifact_path: path to the artifact to be archived.
|
|
86
|
+
"""
|
|
87
|
+
# Convert to absolute path first
|
|
88
|
+
absolute_path = artifact_path.resolve() if not artifact_path.is_absolute() else artifact_path
|
|
89
|
+
|
|
90
|
+
# Calculate the relative path from out_dir for the archive
|
|
91
|
+
if absolute_path.is_relative_to(self.out_dir.absolute()):
|
|
92
|
+
archive_path = absolute_path.relative_to(self.out_dir.absolute())
|
|
93
|
+
else:
|
|
94
|
+
# If not relative to out_dir, just use the name
|
|
95
|
+
archive_path = Path(absolute_path.name)
|
|
96
|
+
|
|
97
|
+
self.archive_artifacts.append(
|
|
98
|
+
self.ArchiveArtifact(
|
|
99
|
+
archive_path=archive_path,
|
|
100
|
+
absolute_path=absolute_path,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def create_archive(self) -> Path:
|
|
105
|
+
"""
|
|
106
|
+
Create a 7z file containing the collected artifacts.
|
|
107
|
+
Returns:
|
|
108
|
+
Path: The path to the created 7z file.
|
|
109
|
+
Raises:
|
|
110
|
+
Exception: If there is an error creating the 7z file.
|
|
111
|
+
"""
|
|
112
|
+
# Construct the full archive path
|
|
113
|
+
archive_path = self.out_dir / self.archive_name
|
|
114
|
+
|
|
115
|
+
# Create output directory if it doesn't exist
|
|
116
|
+
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
# Delete the file if it already exists
|
|
119
|
+
if archive_path.exists():
|
|
120
|
+
archive_path.unlink()
|
|
121
|
+
|
|
122
|
+
if not self.archive_artifacts:
|
|
123
|
+
logger.warning("No artifacts registered for archiving")
|
|
124
|
+
# Create empty 7z file
|
|
125
|
+
with py7zr.SevenZipFile(archive_path, "w") as archive:
|
|
126
|
+
pass
|
|
127
|
+
return archive_path
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
with py7zr.SevenZipFile(archive_path, "w") as archive:
|
|
131
|
+
for artifact in self.archive_artifacts:
|
|
132
|
+
if not artifact.absolute_path.exists():
|
|
133
|
+
logger.warning(f"Artifact {artifact.absolute_path} does not exist, skipping")
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
if artifact.absolute_path.is_file():
|
|
138
|
+
archive.write(artifact.absolute_path, arcname=str(artifact.archive_path))
|
|
139
|
+
elif artifact.absolute_path.is_dir():
|
|
140
|
+
# py7zr can handle directories directly
|
|
141
|
+
archive.writeall(artifact.absolute_path, arcname=str(artifact.archive_path))
|
|
142
|
+
except Exception as file_error:
|
|
143
|
+
logger.warning(f"Failed to add {artifact.absolute_path} to archive: {file_error}")
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
logger.info(f"7z file created at: {archive_path}")
|
|
147
|
+
return archive_path
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(f"Error creating artifacts 7z file: {e}")
|
|
150
|
+
raise e
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ArtifactsArchiver:
|
|
154
|
+
"""
|
|
155
|
+
This class manages multiple ArtifactsArchive instances.
|
|
156
|
+
It provides a unified interface for registering artifacts to different archives.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(self, artifactory_base_url: Optional[str] = None) -> None:
|
|
160
|
+
self.archives: Dict[str, ArtifactsArchive] = {}
|
|
161
|
+
self._target_repos: Dict[str, str] = {}
|
|
162
|
+
self.artifactory_base_url = artifactory_base_url
|
|
163
|
+
|
|
164
|
+
def add_archive(self, out_dir: Path, archive_filename: str, target_repo: Optional[str] = None, archive_name: str = "default") -> ArtifactsArchive:
|
|
165
|
+
"""
|
|
166
|
+
Add a new archive to the archiver.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
out_dir: Output directory for the archive
|
|
170
|
+
archive_filename: Filename for the archive
|
|
171
|
+
target_repo: Target repository path for Artifactory upload (optional)
|
|
172
|
+
archive_name: Name identifier for the archive (defaults to "default")
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
The created ArtifactsArchive instance
|
|
176
|
+
"""
|
|
177
|
+
archive = ArtifactsArchive(out_dir, archive_filename)
|
|
178
|
+
self.archives[archive_name] = archive
|
|
179
|
+
# Store the target repo information for this archive only if provided
|
|
180
|
+
if target_repo is not None:
|
|
181
|
+
self._target_repos[archive_name] = target_repo
|
|
182
|
+
return archive
|
|
183
|
+
|
|
184
|
+
def register(self, artifacts: List[Path], archive_name: str = "default") -> None:
|
|
185
|
+
"""
|
|
186
|
+
Register artifacts for archiving to a specific archive.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
artifacts: List of paths to artifacts (files or directories) to be archived.
|
|
190
|
+
archive_name: Name of the archive to register artifacts to (defaults to "default")
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
KeyError: If the specified archive_name doesn't exist
|
|
194
|
+
"""
|
|
195
|
+
if archive_name not in self.archives:
|
|
196
|
+
raise KeyError(f"Archive '{archive_name}' not found. Available archives: {list(self.archives.keys())}")
|
|
197
|
+
|
|
198
|
+
self.archives[archive_name].register(artifacts)
|
|
199
|
+
|
|
200
|
+
def get_archive(self, archive_name: str) -> ArtifactsArchive:
|
|
201
|
+
"""
|
|
202
|
+
Get a specific archive by name.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
archive_name: Name of the archive to retrieve
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The ArtifactsArchive instance
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
KeyError: If the specified archive_name doesn't exist
|
|
212
|
+
"""
|
|
213
|
+
if archive_name not in self.archives:
|
|
214
|
+
raise KeyError(f"Archive '{archive_name}' not found. Available archives: {list(self.archives.keys())}")
|
|
215
|
+
|
|
216
|
+
return self.archives[archive_name]
|
|
217
|
+
|
|
218
|
+
def get_archive_url(self, archive_name: str = "default") -> Optional[str]:
|
|
219
|
+
"""
|
|
220
|
+
Get the Artifactory URL for a specific archive.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
archive_name: Name of the archive (defaults to "default")
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The full Artifactory URL for the archive, or None if no target repo configured
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
"https://artifactory.example.com/artifactory/my-repo/results/develop/123/result.7z"
|
|
230
|
+
"""
|
|
231
|
+
if archive_name not in self.archives:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
if archive_name not in self._target_repos:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
if self.artifactory_base_url is None:
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
archive = self.archives[archive_name]
|
|
241
|
+
target_repo = self._target_repos[archive_name]
|
|
242
|
+
metadata = self._get_build_metadata()
|
|
243
|
+
|
|
244
|
+
# Construct the URL following the same pattern as create_rt_upload_json
|
|
245
|
+
archive_url = f"{self.artifactory_base_url}/{target_repo}/{metadata.branch_name}/{metadata.build_number}/{archive.archive_name}"
|
|
246
|
+
|
|
247
|
+
return archive_url
|
|
248
|
+
|
|
249
|
+
def create_all_archives(self) -> Dict[str, Path]:
|
|
250
|
+
"""
|
|
251
|
+
Create all registered archives.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Dictionary mapping archive names to their created file paths
|
|
255
|
+
"""
|
|
256
|
+
created_archives = {}
|
|
257
|
+
for archive_name, archive in self.archives.items():
|
|
258
|
+
created_archives[archive_name] = archive.create_archive()
|
|
259
|
+
return created_archives
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def calculate_retention_period(branch_name: str, is_tag: bool) -> int:
|
|
263
|
+
"""
|
|
264
|
+
Calculate the retention period in days based on branch name or tag.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
branch_name: The name of the branch
|
|
268
|
+
is_tag: Whether this is a tag build
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Retention period in days:
|
|
272
|
+
- 84 days for "develop" branch
|
|
273
|
+
- -1 (infinite) for release branches (release/*)
|
|
274
|
+
- -1 (infinite) for tags
|
|
275
|
+
- 28 days for everything else (PRs, feature branches, etc.)
|
|
276
|
+
"""
|
|
277
|
+
if is_tag:
|
|
278
|
+
return -1 # Infinite retention for tags
|
|
279
|
+
elif branch_name == "develop":
|
|
280
|
+
return 84 # Length of a PI (Program Increment)
|
|
281
|
+
elif branch_name.startswith("release/"):
|
|
282
|
+
return -1 # Infinite retention for release branches
|
|
283
|
+
else:
|
|
284
|
+
return 28 # 4 weeks for PRs, feature branches, and other branches
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _get_build_metadata() -> BuildMetadata:
|
|
288
|
+
"""
|
|
289
|
+
Get build metadata from environment variables or defaults.
|
|
290
|
+
|
|
291
|
+
Detects Jenkins environment variables when available, otherwise falls back
|
|
292
|
+
to local development defaults.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
BuildMetadata instance containing:
|
|
296
|
+
- branch_name: The branch name, PR identifier (e.g., "PR-123"), or tag name
|
|
297
|
+
- build_number: The build number or "local_build"
|
|
298
|
+
- is_tag: Whether this is a tag build
|
|
299
|
+
- pr_number: The PR number (without "PR-" prefix) for pull requests, None otherwise
|
|
300
|
+
"""
|
|
301
|
+
branch_name = "local_branch"
|
|
302
|
+
build_number = "local_build"
|
|
303
|
+
is_tag = False
|
|
304
|
+
pr_number = None
|
|
305
|
+
|
|
306
|
+
if os.environ.get("JENKINS_URL"):
|
|
307
|
+
change_id = os.environ.get("CHANGE_ID")
|
|
308
|
+
jenkins_branch_name = os.environ.get("BRANCH_NAME")
|
|
309
|
+
jenkins_build_number = os.environ.get("BUILD_NUMBER")
|
|
310
|
+
tag_name = os.environ.get("TAG_NAME")
|
|
311
|
+
|
|
312
|
+
if change_id:
|
|
313
|
+
# Pull request case
|
|
314
|
+
branch_name = f"PR-{change_id}"
|
|
315
|
+
pr_number = change_id
|
|
316
|
+
elif tag_name:
|
|
317
|
+
# Tag build case
|
|
318
|
+
branch_name = tag_name
|
|
319
|
+
is_tag = True
|
|
320
|
+
elif jenkins_branch_name:
|
|
321
|
+
# Regular branch case
|
|
322
|
+
branch_name = jenkins_branch_name
|
|
323
|
+
|
|
324
|
+
if jenkins_build_number:
|
|
325
|
+
build_number = jenkins_build_number
|
|
326
|
+
|
|
327
|
+
return BuildMetadata(
|
|
328
|
+
branch_name=branch_name,
|
|
329
|
+
build_number=build_number,
|
|
330
|
+
is_tag=is_tag,
|
|
331
|
+
pr_number=pr_number,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _get_git_metadata() -> GitMetadata:
|
|
336
|
+
"""
|
|
337
|
+
Get git metadata from environment variables or git commands.
|
|
338
|
+
|
|
339
|
+
Attempts to retrieve git information in the following order:
|
|
340
|
+
1. Environment variables (GIT_COMMIT, GIT_URL) - typically set by Jenkins Git plugin
|
|
341
|
+
2. Git commands as fallback - executed locally using git CLI
|
|
342
|
+
|
|
343
|
+
The commit message captured is only the subject line (first line), not the full message.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
GitMetadata instance containing:
|
|
347
|
+
- commit_id: The git commit SHA, or None if unavailable
|
|
348
|
+
- commit_message: The commit subject line (first line only), or None if unavailable
|
|
349
|
+
- repository_url: The git repository URL, or None if unavailable
|
|
350
|
+
"""
|
|
351
|
+
commit_id = None
|
|
352
|
+
commit_message = None
|
|
353
|
+
repository_url = None
|
|
354
|
+
|
|
355
|
+
# Try environment variables first (Jenkins Git plugin)
|
|
356
|
+
env_commit = os.environ.get("GIT_COMMIT")
|
|
357
|
+
env_url = os.environ.get("GIT_URL")
|
|
358
|
+
|
|
359
|
+
if env_commit:
|
|
360
|
+
commit_id = env_commit if env_commit.strip() else None
|
|
361
|
+
if env_url:
|
|
362
|
+
repository_url = env_url if env_url.strip() else None
|
|
363
|
+
|
|
364
|
+
# Fallback to git commands if environment variables not available
|
|
365
|
+
# Get commit ID
|
|
366
|
+
if not commit_id:
|
|
367
|
+
try:
|
|
368
|
+
result = SubprocessExecutor(["git", "rev-parse", "HEAD"]).execute(handle_errors=False)
|
|
369
|
+
if result and result.returncode == 0:
|
|
370
|
+
value = result.stdout.strip()
|
|
371
|
+
commit_id = value if value else None
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.warning(f"Failed to get commit ID from git: {e}")
|
|
374
|
+
|
|
375
|
+
# Get commit message (subject line only)
|
|
376
|
+
if not commit_message:
|
|
377
|
+
try:
|
|
378
|
+
result = SubprocessExecutor(["git", "log", "-1", "--format=%s"]).execute(handle_errors=False)
|
|
379
|
+
if result and result.returncode == 0:
|
|
380
|
+
value = result.stdout.strip()
|
|
381
|
+
commit_message = value if value else None
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.warning(f"Failed to get commit message from git: {e}")
|
|
384
|
+
|
|
385
|
+
# Get repository URL
|
|
386
|
+
if not repository_url:
|
|
387
|
+
try:
|
|
388
|
+
result = SubprocessExecutor(["git", "config", "--get", "remote.origin.url"]).execute(handle_errors=False)
|
|
389
|
+
if result and result.returncode == 0:
|
|
390
|
+
value = result.stdout.strip()
|
|
391
|
+
repository_url = value if value else None
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.warning(f"Failed to get repository URL from git: {e}")
|
|
394
|
+
|
|
395
|
+
return GitMetadata(
|
|
396
|
+
commit_id=commit_id,
|
|
397
|
+
commit_message=commit_message,
|
|
398
|
+
repository_url=repository_url,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def create_rt_upload_json(self, out_dir: Path) -> Path:
|
|
402
|
+
"""
|
|
403
|
+
Create a single rt-upload.json file containing all archives.
|
|
404
|
+
|
|
405
|
+
This function replicates the logic from the Jenkinsfile for determining the RT_TARGET
|
|
406
|
+
and creating the upload specification file. It uses Jenkins environment variables
|
|
407
|
+
when available, otherwise falls back to default values.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
output_dir: Directory where the rt-upload.json file will be created
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Path to the created rt-upload.json file
|
|
414
|
+
"""
|
|
415
|
+
# Get build metadata from environment or defaults
|
|
416
|
+
metadata = self._get_build_metadata()
|
|
417
|
+
|
|
418
|
+
# Calculate retention period based on branch/tag
|
|
419
|
+
retention_period = self.calculate_retention_period(metadata.branch_name, metadata.is_tag)
|
|
420
|
+
|
|
421
|
+
# Create the files array for Artifactory upload format
|
|
422
|
+
files_array = []
|
|
423
|
+
|
|
424
|
+
for archive_name, archive in self.archives.items():
|
|
425
|
+
if archive_name in self._target_repos:
|
|
426
|
+
target_repo = self._target_repos[archive_name]
|
|
427
|
+
|
|
428
|
+
# Construct the RT target path
|
|
429
|
+
rt_target = f"{target_repo}/{metadata.branch_name}/{metadata.build_number}/"
|
|
430
|
+
|
|
431
|
+
# Add this archive to the files array with retention_period property
|
|
432
|
+
files_array.append(
|
|
433
|
+
{
|
|
434
|
+
"pattern": archive.archive_name,
|
|
435
|
+
"target": rt_target,
|
|
436
|
+
"recursive": "false",
|
|
437
|
+
"flat": "false",
|
|
438
|
+
"regexp": "false",
|
|
439
|
+
"props": f"retention_period={retention_period}",
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Create the single rt-upload.json file
|
|
444
|
+
json_path = out_dir / "rt-upload.json"
|
|
445
|
+
|
|
446
|
+
spec = {"files": files_array}
|
|
447
|
+
|
|
448
|
+
with open(json_path, "w") as f:
|
|
449
|
+
json.dump(spec, f, indent=4)
|
|
450
|
+
|
|
451
|
+
return json_path
|
|
452
|
+
|
|
453
|
+
def create_artifacts_json(self, variant: str, out_dir: Path) -> Path:
|
|
454
|
+
"""
|
|
455
|
+
Create an initial artifacts.json file with build metadata structure.
|
|
456
|
+
|
|
457
|
+
This function creates a fresh artifacts.json file with build metadata
|
|
458
|
+
but no artifacts. Use update_artifacts_json() to add artifact categories.
|
|
459
|
+
It uses Jenkins environment variables when available, otherwise falls back to default values.
|
|
460
|
+
|
|
461
|
+
The JSON file includes conditional keys based on the build type:
|
|
462
|
+
- For pull requests: includes "pull_request" key with the PR number (e.g., "117")
|
|
463
|
+
- For tag builds: includes "tag" key with the tag name (e.g., "v1.2.3")
|
|
464
|
+
- For regular branch builds: includes "branch" key with the branch name (e.g., "develop")
|
|
465
|
+
|
|
466
|
+
Optional fields (included only if available):
|
|
467
|
+
- build_url: Jenkins build URL from BUILD_URL environment variable
|
|
468
|
+
- commit_id: Git commit SHA from GIT_COMMIT env var or git rev-parse HEAD
|
|
469
|
+
- commit_message: Git commit subject line from git log (first line only)
|
|
470
|
+
- repository_url: Git repository URL from GIT_URL env var or git config
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
variant: The variant name (e.g., "Disco")
|
|
474
|
+
out_dir: Directory where the artifacts.json file will be created
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Path to the created artifacts.json file
|
|
478
|
+
|
|
479
|
+
Raises:
|
|
480
|
+
ValueError: If variant is empty or None
|
|
481
|
+
"""
|
|
482
|
+
# Input validation
|
|
483
|
+
if not variant or not variant.strip():
|
|
484
|
+
raise ValueError("Variant name cannot be empty or None")
|
|
485
|
+
|
|
486
|
+
# Get metadata from environment or defaults
|
|
487
|
+
build_metadata = self._get_build_metadata()
|
|
488
|
+
git_metadata = self._get_git_metadata()
|
|
489
|
+
|
|
490
|
+
# Create the initial artifacts.json structure with base metadata
|
|
491
|
+
artifacts_data: Dict[str, Any] = {
|
|
492
|
+
"variant": variant,
|
|
493
|
+
"build_timestamp": datetime.now(timezone.utc).isoformat(timespec="seconds") + "Z",
|
|
494
|
+
"build_number": build_metadata.build_number,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
# Add build_url if available
|
|
498
|
+
build_url = os.environ.get("BUILD_URL")
|
|
499
|
+
if build_url:
|
|
500
|
+
artifacts_data["build_url"] = build_url
|
|
501
|
+
|
|
502
|
+
# Add conditional keys based on build type
|
|
503
|
+
if build_metadata.pr_number:
|
|
504
|
+
# Pull request build
|
|
505
|
+
artifacts_data["pull_request"] = build_metadata.pr_number
|
|
506
|
+
elif build_metadata.is_tag:
|
|
507
|
+
# Tag build
|
|
508
|
+
artifacts_data["tag"] = build_metadata.branch_name
|
|
509
|
+
else:
|
|
510
|
+
# Regular branch build (or local build)
|
|
511
|
+
artifacts_data["branch"] = build_metadata.branch_name
|
|
512
|
+
|
|
513
|
+
# Add git metadata if available
|
|
514
|
+
if git_metadata.commit_id:
|
|
515
|
+
artifacts_data["commit_id"] = git_metadata.commit_id
|
|
516
|
+
if git_metadata.commit_message:
|
|
517
|
+
artifacts_data["commit_message"] = git_metadata.commit_message
|
|
518
|
+
if git_metadata.repository_url:
|
|
519
|
+
artifacts_data["repository_url"] = git_metadata.repository_url
|
|
520
|
+
|
|
521
|
+
# Add empty artifacts dictionary
|
|
522
|
+
artifacts_data["artifacts"] = {}
|
|
523
|
+
|
|
524
|
+
# Create the artifacts.json file
|
|
525
|
+
json_path = out_dir / "artifacts.json"
|
|
526
|
+
json_path.parent.mkdir(parents=True, exist_ok=True)
|
|
527
|
+
|
|
528
|
+
with open(json_path, "w") as f:
|
|
529
|
+
json.dump(artifacts_data, f, indent=2)
|
|
530
|
+
|
|
531
|
+
return json_path
|
|
532
|
+
|
|
533
|
+
def update_artifacts_json(self, category: str, artifacts: Dict[str, str], artifacts_json_path: Path) -> Path:
|
|
534
|
+
"""
|
|
535
|
+
Add or update artifacts in a specific category for the artifacts.json file.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
category: The artifact category (e.g., "test_reports", "sca_reports", "build_binaries")
|
|
539
|
+
artifacts: Dictionary mapping artifact names to their URLs/paths
|
|
540
|
+
artifacts_json_path: Path to the artifacts.json file to be updated
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
Path to the updated artifacts.json file
|
|
544
|
+
|
|
545
|
+
Raises:
|
|
546
|
+
ValueError: If category is empty, artifacts dictionary is empty, or JSON structure is invalid
|
|
547
|
+
FileNotFoundError: If artifacts.json file does not exist
|
|
548
|
+
"""
|
|
549
|
+
# Input validation
|
|
550
|
+
if not category or not category.strip():
|
|
551
|
+
raise ValueError("Category name cannot be empty or None")
|
|
552
|
+
if not artifacts:
|
|
553
|
+
raise ValueError("Artifacts dictionary cannot be empty")
|
|
554
|
+
|
|
555
|
+
# Check if artifacts.json file exists
|
|
556
|
+
if not artifacts_json_path.exists():
|
|
557
|
+
raise FileNotFoundError(f"artifacts.json file does not exist at {artifacts_json_path}. Please create it first using create_artifacts_json().")
|
|
558
|
+
|
|
559
|
+
# Read existing artifacts.json file
|
|
560
|
+
try:
|
|
561
|
+
with open(artifacts_json_path) as f:
|
|
562
|
+
artifacts_data = json.load(f)
|
|
563
|
+
except json.JSONDecodeError as e:
|
|
564
|
+
raise ValueError(f"Could not parse artifacts.json: {e}") from e
|
|
565
|
+
except OSError as e:
|
|
566
|
+
raise ValueError(f"Could not read artifacts.json: {e}") from e
|
|
567
|
+
|
|
568
|
+
# Validate that the file has the expected structure
|
|
569
|
+
if not artifacts_data or "artifacts" not in artifacts_data:
|
|
570
|
+
raise ValueError("artifacts.json file has invalid structure. Expected 'artifacts' section not found.")
|
|
571
|
+
|
|
572
|
+
# Update the specific category with new artifacts
|
|
573
|
+
if category in artifacts_data["artifacts"]:
|
|
574
|
+
artifacts_data["artifacts"][category].update(artifacts)
|
|
575
|
+
else:
|
|
576
|
+
artifacts_data["artifacts"][category] = artifacts.copy()
|
|
577
|
+
|
|
578
|
+
# Write the updated data back to the file
|
|
579
|
+
with open(artifacts_json_path, "w") as f:
|
|
580
|
+
json.dump(artifacts_data, f, indent=2)
|
|
581
|
+
|
|
582
|
+
return artifacts_json_path
|
|
583
|
+
|
|
584
|
+
def list_archives(self) -> List[str]:
|
|
585
|
+
"""
|
|
586
|
+
Get a list of all archive names.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
List of archive names
|
|
590
|
+
"""
|
|
591
|
+
return list(self.archives.keys())
|
|
592
|
+
|
|
593
|
+
def create_archive(self, archive_name: str = "default") -> Path:
|
|
594
|
+
"""
|
|
595
|
+
Create a specific archive (convenience method for single-archive use case).
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
archive_name: Name of the archive to create (defaults to "default")
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
Path to the created archive file
|
|
602
|
+
|
|
603
|
+
Raises:
|
|
604
|
+
KeyError: If the specified archive_name doesn't exist
|
|
605
|
+
"""
|
|
606
|
+
if archive_name not in self.archives:
|
|
607
|
+
raise KeyError(f"Archive '{archive_name}' not found. Available archives: {list(self.archives.keys())}")
|
|
608
|
+
|
|
609
|
+
return self.archives[archive_name].create_archive()
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# Example usage:
|
|
613
|
+
#
|
|
614
|
+
# ## Simple single-archive use case with target repo:
|
|
615
|
+
# archiver = ArtifactsArchiver()
|
|
616
|
+
# archiver.add_archive(Path("./build/output"), "results.7z", "my-repo/results") # uses "default" name
|
|
617
|
+
# archiver.register([Path("./build/test_report.xml"), Path("./build/coverage.html")]) # registers to "default"
|
|
618
|
+
# archive_path = archiver.create_archive() # creates the "default" archive
|
|
619
|
+
# upload_json = archiver.create_rt_upload_json(Path("./build/output"))
|
|
620
|
+
#
|
|
621
|
+
# ## Simple single-archive use case without target repo (archive only):
|
|
622
|
+
# archiver = ArtifactsArchiver()
|
|
623
|
+
# archiver.add_archive(Path("./build/output"), "results.7z") # no target repo, uses "default" name
|
|
624
|
+
# archiver.register([Path("./build/test_report.xml"), Path("./build/coverage.html")])
|
|
625
|
+
# archive_path = archiver.create_archive() # creates the "default" archive
|
|
626
|
+
# # upload_json = archiver.create_rt_upload_json(Path("./build/output")) # would create empty JSON
|
|
627
|
+
#
|
|
628
|
+
# ## Multi-archive use case:
|
|
629
|
+
# archiver = ArtifactsArchiver()
|
|
630
|
+
# archiver.add_archive(Path("./build/output"), "test_results.7z", "my-repo/test-results", "test_results")
|
|
631
|
+
# archiver.add_archive(Path("./build/output"), "coverage.7z", "my-repo/coverage", "coverage_reports")
|
|
632
|
+
# archiver.add_archive(Path("./build/output"), "docs.7z", None, "documentation") # no target repo for docs
|
|
633
|
+
#
|
|
634
|
+
# archiver.register([Path("./build/test_report.xml")], "test_results")
|
|
635
|
+
# archiver.register([Path("./build/coverage.html")], "coverage_reports")
|
|
636
|
+
# archiver.register([Path("./build/docs/")], "documentation")
|
|
637
|
+
#
|
|
638
|
+
# created_files = archiver.create_all_archives()
|
|
639
|
+
# upload_json = archiver.create_rt_upload_json(Path("./build/output")) # only includes archives with target repos
|
|
640
|
+
#
|
|
641
|
+
# ## Artifacts.json use case (variant-specific metadata):
|
|
642
|
+
# archiver = ArtifactsArchiver()
|
|
643
|
+
# variant = "Disco"
|
|
644
|
+
# out_dir = Path("./build/output")
|
|
645
|
+
#
|
|
646
|
+
# # Create initial artifacts.json file first, then add categories
|
|
647
|
+
# # The resulting JSON will contain conditional keys based on build type:
|
|
648
|
+
# # - For PRs: {"variant": "Disco", "build_timestamp": "...", "build_number": "123", "pull_request": "117", "artifacts": {}}
|
|
649
|
+
# # - For tags: {"variant": "Disco", "build_timestamp": "...", "build_number": "123", "tag": "v1.2.3", "artifacts": {}}
|
|
650
|
+
# # - For branches: {"variant": "Disco", "build_timestamp": "...", "build_number": "123", "branch": "develop", "artifacts": {}}
|
|
651
|
+
# artifacts_json_path = archiver.create_artifacts_json(variant, out_dir)
|
|
652
|
+
# archiver.update_artifacts_json("test_reports", test_reports, artifacts_json_path)
|
|
653
|
+
# archiver.update_artifacts_json("sca_reports", sca_reports, artifacts_json_path)
|
|
654
|
+
# archiver.update_artifacts_json("build_binaries", build_binaries, artifacts_json_path)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "7.12.1"
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Dict, List, Optional
|
|
6
|
-
|
|
7
|
-
import py7zr
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class ArtifactsArchive:
|
|
11
|
-
"""
|
|
12
|
-
This class represents a single archive containing artifacts.
|
|
13
|
-
It collects artifacts to be packed and archived.
|
|
14
|
-
|
|
15
|
-
Currently supports 7z and Artifactory.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
@dataclass
|
|
19
|
-
class ArchiveArtifact:
|
|
20
|
-
"""
|
|
21
|
-
Represents a single artifact to be archived.
|
|
22
|
-
This class holds the archive path (relative to the output directory of the 7z archive)
|
|
23
|
-
and the absolute path of the artifact.
|
|
24
|
-
It is used to ensure that artifacts are correctly archived with their intended paths.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
archive_path: Path
|
|
28
|
-
absolute_path: Path
|
|
29
|
-
|
|
30
|
-
def __init__(self, out_dir: Path, archive_name: str) -> None:
|
|
31
|
-
self.out_dir: Path = out_dir
|
|
32
|
-
self.archive_name: str = archive_name
|
|
33
|
-
self.archive_artifacts: List[ArtifactsArchive.ArchiveArtifact] = []
|
|
34
|
-
|
|
35
|
-
def register(self, artifacts: List[Path]) -> None:
|
|
36
|
-
"""
|
|
37
|
-
Register artifacts for archiving.
|
|
38
|
-
Args:
|
|
39
|
-
artifacts: List of paths to artifacts (files or directories) to be archived.
|
|
40
|
-
"""
|
|
41
|
-
for artifact in artifacts:
|
|
42
|
-
self._add_artifact(artifact)
|
|
43
|
-
|
|
44
|
-
def _add_artifact(self, artifact_path: Path) -> None:
|
|
45
|
-
"""
|
|
46
|
-
Add an artifact (file or directory) to the archive list.
|
|
47
|
-
Args:
|
|
48
|
-
artifact_path: path to the artifact to be archived.
|
|
49
|
-
"""
|
|
50
|
-
# Convert to absolute path first
|
|
51
|
-
absolute_path = artifact_path.resolve() if not artifact_path.is_absolute() else artifact_path
|
|
52
|
-
|
|
53
|
-
# Calculate the relative path from out_dir for the archive
|
|
54
|
-
if absolute_path.is_relative_to(self.out_dir.absolute()):
|
|
55
|
-
archive_path = absolute_path.relative_to(self.out_dir.absolute())
|
|
56
|
-
else:
|
|
57
|
-
# If not relative to out_dir, just use the name
|
|
58
|
-
archive_path = Path(absolute_path.name)
|
|
59
|
-
|
|
60
|
-
self.archive_artifacts.append(
|
|
61
|
-
self.ArchiveArtifact(
|
|
62
|
-
archive_path=archive_path,
|
|
63
|
-
absolute_path=absolute_path,
|
|
64
|
-
)
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
def create_archive(self) -> Path:
|
|
68
|
-
"""
|
|
69
|
-
Create a 7z file containing the collected artifacts.
|
|
70
|
-
Returns:
|
|
71
|
-
Path: The path to the created 7z file.
|
|
72
|
-
Raises:
|
|
73
|
-
Exception: If there is an error creating the 7z file.
|
|
74
|
-
"""
|
|
75
|
-
# Construct the full archive path
|
|
76
|
-
archive_path = self.out_dir / self.archive_name
|
|
77
|
-
|
|
78
|
-
# Create output directory if it doesn't exist
|
|
79
|
-
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
-
|
|
81
|
-
# Delete the file if it already exists
|
|
82
|
-
if archive_path.exists():
|
|
83
|
-
archive_path.unlink()
|
|
84
|
-
|
|
85
|
-
if not self.archive_artifacts:
|
|
86
|
-
print("Warning: No artifacts registered for archiving")
|
|
87
|
-
# Create empty 7z file
|
|
88
|
-
with py7zr.SevenZipFile(archive_path, "w") as archive:
|
|
89
|
-
pass
|
|
90
|
-
return archive_path
|
|
91
|
-
|
|
92
|
-
try:
|
|
93
|
-
with py7zr.SevenZipFile(archive_path, "w") as archive:
|
|
94
|
-
for artifact in self.archive_artifacts:
|
|
95
|
-
if not artifact.absolute_path.exists():
|
|
96
|
-
print(f"Warning: Artifact {artifact.absolute_path} does not exist, skipping")
|
|
97
|
-
continue
|
|
98
|
-
|
|
99
|
-
try:
|
|
100
|
-
if artifact.absolute_path.is_file():
|
|
101
|
-
archive.write(artifact.absolute_path, arcname=str(artifact.archive_path))
|
|
102
|
-
elif artifact.absolute_path.is_dir():
|
|
103
|
-
# py7zr can handle directories directly
|
|
104
|
-
archive.writeall(artifact.absolute_path, arcname=str(artifact.archive_path))
|
|
105
|
-
except Exception as file_error:
|
|
106
|
-
print(f"Warning: Failed to add {artifact.absolute_path} to archive: {file_error}")
|
|
107
|
-
continue
|
|
108
|
-
|
|
109
|
-
print(f"7z file created at: {archive_path}")
|
|
110
|
-
return archive_path
|
|
111
|
-
except Exception as e:
|
|
112
|
-
print(f"Error creating artifacts 7z file: {e}")
|
|
113
|
-
raise e
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class ArtifactsArchiver:
|
|
117
|
-
"""
|
|
118
|
-
This class manages multiple ArtifactsArchive instances.
|
|
119
|
-
It provides a unified interface for registering artifacts to different archives.
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
|
-
def __init__(self) -> None:
|
|
123
|
-
self.archives: Dict[str, ArtifactsArchive] = {}
|
|
124
|
-
self._target_repos: Dict[str, str] = {}
|
|
125
|
-
|
|
126
|
-
def add_archive(self, out_dir: Path, archive_filename: str, target_repo: Optional[str] = None, archive_name: str = "default") -> ArtifactsArchive:
|
|
127
|
-
"""
|
|
128
|
-
Add a new archive to the archiver.
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
out_dir: Output directory for the archive
|
|
132
|
-
archive_filename: Filename for the archive
|
|
133
|
-
target_repo: Target repository path for Artifactory upload (optional)
|
|
134
|
-
archive_name: Name identifier for the archive (defaults to "default")
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
The created ArtifactsArchive instance
|
|
138
|
-
"""
|
|
139
|
-
archive = ArtifactsArchive(out_dir, archive_filename)
|
|
140
|
-
self.archives[archive_name] = archive
|
|
141
|
-
# Store the target repo information for this archive only if provided
|
|
142
|
-
if target_repo is not None:
|
|
143
|
-
self._target_repos[archive_name] = target_repo
|
|
144
|
-
return archive
|
|
145
|
-
|
|
146
|
-
def register(self, artifacts: List[Path], archive_name: str = "default") -> None:
|
|
147
|
-
"""
|
|
148
|
-
Register artifacts for archiving to a specific archive.
|
|
149
|
-
|
|
150
|
-
Args:
|
|
151
|
-
artifacts: List of paths to artifacts (files or directories) to be archived.
|
|
152
|
-
archive_name: Name of the archive to register artifacts to (defaults to "default")
|
|
153
|
-
|
|
154
|
-
Raises:
|
|
155
|
-
KeyError: If the specified archive_name doesn't exist
|
|
156
|
-
"""
|
|
157
|
-
if archive_name not in self.archives:
|
|
158
|
-
raise KeyError(f"Archive '{archive_name}' not found. Available archives: {list(self.archives.keys())}")
|
|
159
|
-
|
|
160
|
-
self.archives[archive_name].register(artifacts)
|
|
161
|
-
|
|
162
|
-
def get_archive(self, archive_name: str) -> ArtifactsArchive:
|
|
163
|
-
"""
|
|
164
|
-
Get a specific archive by name.
|
|
165
|
-
|
|
166
|
-
Args:
|
|
167
|
-
archive_name: Name of the archive to retrieve
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
The ArtifactsArchive instance
|
|
171
|
-
|
|
172
|
-
Raises:
|
|
173
|
-
KeyError: If the specified archive_name doesn't exist
|
|
174
|
-
"""
|
|
175
|
-
if archive_name not in self.archives:
|
|
176
|
-
raise KeyError(f"Archive '{archive_name}' not found. Available archives: {list(self.archives.keys())}")
|
|
177
|
-
|
|
178
|
-
return self.archives[archive_name]
|
|
179
|
-
|
|
180
|
-
def create_all_archives(self) -> Dict[str, Path]:
|
|
181
|
-
"""
|
|
182
|
-
Create all registered archives.
|
|
183
|
-
|
|
184
|
-
Returns:
|
|
185
|
-
Dictionary mapping archive names to their created file paths
|
|
186
|
-
"""
|
|
187
|
-
created_archives = {}
|
|
188
|
-
for archive_name, archive in self.archives.items():
|
|
189
|
-
created_archives[archive_name] = archive.create_archive()
|
|
190
|
-
return created_archives
|
|
191
|
-
|
|
192
|
-
def create_rt_upload_json(self, out_dir: Path) -> Path:
|
|
193
|
-
"""
|
|
194
|
-
Create a single rt-upload.json file containing all archives.
|
|
195
|
-
|
|
196
|
-
This function replicates the logic from the Jenkinsfile for determining the RT_TARGET
|
|
197
|
-
and creating the upload specification file. It uses Jenkins environment variables
|
|
198
|
-
when available, otherwise falls back to default values.
|
|
199
|
-
|
|
200
|
-
Args:
|
|
201
|
-
output_dir: Directory where the rt-upload.json file will be created
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
Path to the created rt-upload.json file
|
|
205
|
-
"""
|
|
206
|
-
# Set local defaults first
|
|
207
|
-
change_id = None
|
|
208
|
-
branch_name = "local_branch"
|
|
209
|
-
build_number = "local_build"
|
|
210
|
-
|
|
211
|
-
# Adapt values when Jenkins environment is detected
|
|
212
|
-
# TODO: check if an existing library can be used for CI context detection
|
|
213
|
-
if os.environ.get("JENKINS_URL"):
|
|
214
|
-
change_id = os.environ.get("CHANGE_ID")
|
|
215
|
-
jenkins_branch_name = os.environ.get("BRANCH_NAME")
|
|
216
|
-
jenkins_build_number = os.environ.get("BUILD_NUMBER")
|
|
217
|
-
tag_name = os.environ.get("TAG_NAME")
|
|
218
|
-
|
|
219
|
-
if change_id:
|
|
220
|
-
# Pull request case
|
|
221
|
-
branch_name = f"PR-{change_id}"
|
|
222
|
-
elif tag_name:
|
|
223
|
-
# Tag build case
|
|
224
|
-
branch_name = tag_name
|
|
225
|
-
elif jenkins_branch_name:
|
|
226
|
-
# Regular branch case
|
|
227
|
-
branch_name = jenkins_branch_name
|
|
228
|
-
|
|
229
|
-
if jenkins_build_number:
|
|
230
|
-
build_number = jenkins_build_number
|
|
231
|
-
|
|
232
|
-
# Create the files array for Artifactory upload format
|
|
233
|
-
files_array = []
|
|
234
|
-
|
|
235
|
-
for archive_name, archive in self.archives.items():
|
|
236
|
-
if archive_name in self._target_repos:
|
|
237
|
-
target_repo = self._target_repos[archive_name]
|
|
238
|
-
|
|
239
|
-
# Construct the RT target path
|
|
240
|
-
rt_target = f"{target_repo}/{branch_name}/{build_number}/"
|
|
241
|
-
|
|
242
|
-
# Add this archive to the files array
|
|
243
|
-
files_array.append(
|
|
244
|
-
{
|
|
245
|
-
"pattern": archive.archive_name,
|
|
246
|
-
"target": rt_target,
|
|
247
|
-
"recursive": "false",
|
|
248
|
-
"flat": "false",
|
|
249
|
-
"regexp": "false",
|
|
250
|
-
}
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
# Create the single rt-upload.json file
|
|
254
|
-
json_path = out_dir / "rt-upload.json"
|
|
255
|
-
|
|
256
|
-
spec = {"files": files_array}
|
|
257
|
-
|
|
258
|
-
with open(json_path, "w") as f:
|
|
259
|
-
json.dump(spec, f, indent=4)
|
|
260
|
-
|
|
261
|
-
return json_path
|
|
262
|
-
|
|
263
|
-
def list_archives(self) -> List[str]:
|
|
264
|
-
"""
|
|
265
|
-
Get a list of all archive names.
|
|
266
|
-
|
|
267
|
-
Returns:
|
|
268
|
-
List of archive names
|
|
269
|
-
"""
|
|
270
|
-
return list(self.archives.keys())
|
|
271
|
-
|
|
272
|
-
def create_archive(self, archive_name: str = "default") -> Path:
|
|
273
|
-
"""
|
|
274
|
-
Create a specific archive (convenience method for single-archive use case).
|
|
275
|
-
|
|
276
|
-
Args:
|
|
277
|
-
archive_name: Name of the archive to create (defaults to "default")
|
|
278
|
-
|
|
279
|
-
Returns:
|
|
280
|
-
Path to the created archive file
|
|
281
|
-
|
|
282
|
-
Raises:
|
|
283
|
-
KeyError: If the specified archive_name doesn't exist
|
|
284
|
-
"""
|
|
285
|
-
if archive_name not in self.archives:
|
|
286
|
-
raise KeyError(f"Archive '{archive_name}' not found. Available archives: {list(self.archives.keys())}")
|
|
287
|
-
|
|
288
|
-
return self.archives[archive_name].create_archive()
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
# Example usage:
|
|
292
|
-
#
|
|
293
|
-
# ## Simple single-archive use case with target repo:
|
|
294
|
-
# archiver = ArtifactsArchiver()
|
|
295
|
-
# archiver.add_archive(Path("./build/output"), "results.7z", "my-repo/results") # uses "default" name
|
|
296
|
-
# archiver.register([Path("./build/test_report.xml"), Path("./build/coverage.html")]) # registers to "default"
|
|
297
|
-
# archive_path = archiver.create_archive() # creates the "default" archive
|
|
298
|
-
# upload_json = archiver.create_rt_upload_json(Path("./build/output"))
|
|
299
|
-
#
|
|
300
|
-
# ## Simple single-archive use case without target repo (archive only):
|
|
301
|
-
# archiver = ArtifactsArchiver()
|
|
302
|
-
# archiver.add_archive(Path("./build/output"), "results.7z") # no target repo, uses "default" name
|
|
303
|
-
# archiver.register([Path("./build/test_report.xml"), Path("./build/coverage.html")])
|
|
304
|
-
# archive_path = archiver.create_archive() # creates the "default" archive
|
|
305
|
-
# # upload_json = archiver.create_rt_upload_json(Path("./build/output")) # would create empty JSON
|
|
306
|
-
#
|
|
307
|
-
# ## Multi-archive use case:
|
|
308
|
-
# archiver = ArtifactsArchiver()
|
|
309
|
-
# archiver.add_archive(Path("./build/output"), "test_results.7z", "my-repo/test-results", "test_results")
|
|
310
|
-
# archiver.add_archive(Path("./build/output"), "coverage.7z", "my-repo/coverage", "coverage_reports")
|
|
311
|
-
# archiver.add_archive(Path("./build/output"), "docs.7z", None, "documentation") # no target repo for docs
|
|
312
|
-
#
|
|
313
|
-
# archiver.register([Path("./build/test_report.xml")], "test_results")
|
|
314
|
-
# archiver.register([Path("./build/coverage.html")], "coverage_reports")
|
|
315
|
-
# archiver.register([Path("./build/docs/")], "documentation")
|
|
316
|
-
#
|
|
317
|
-
# created_files = archiver.create_all_archives()
|
|
318
|
-
# upload_json = archiver.create_rt_upload_json(Path("./build/output")) # only includes archives with target repos
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/application/src/main/src/main.c
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.vscode/cmake-kits.json
RENAMED
|
File without changes
|
{spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.vscode/extensions.json
RENAMED
|
File without changes
|
{spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.vscode/launch.json
RENAMED
|
File without changes
|
{spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.vscode/settings.json
RENAMED
|
File without changes
|
{spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/.vscode/tasks.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/Doxyfile.in
RENAMED
|
File without changes
|
{spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/common/index.md
RENAMED
|
File without changes
|
{spl_core-7.12.1 → spl_core-7.14.0}/src/spl_core/kickstart/templates/project/doc/components/index.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|