relenv 0.21.2__py3-none-any.whl → 0.22.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. relenv/__init__.py +14 -2
  2. relenv/__main__.py +12 -6
  3. relenv/_resources/xz/config.h +148 -0
  4. relenv/_resources/xz/readme.md +4 -0
  5. relenv/build/__init__.py +28 -30
  6. relenv/build/common/__init__.py +50 -0
  7. relenv/build/common/_sysconfigdata_template.py +72 -0
  8. relenv/build/common/builder.py +907 -0
  9. relenv/build/common/builders.py +163 -0
  10. relenv/build/common/download.py +324 -0
  11. relenv/build/common/install.py +609 -0
  12. relenv/build/common/ui.py +432 -0
  13. relenv/build/darwin.py +128 -14
  14. relenv/build/linux.py +292 -74
  15. relenv/build/windows.py +123 -169
  16. relenv/buildenv.py +48 -17
  17. relenv/check.py +10 -5
  18. relenv/common.py +489 -165
  19. relenv/create.py +147 -7
  20. relenv/fetch.py +16 -4
  21. relenv/manifest.py +15 -7
  22. relenv/python-versions.json +329 -0
  23. relenv/pyversions.py +817 -30
  24. relenv/relocate.py +101 -55
  25. relenv/runtime.py +452 -282
  26. relenv/toolchain.py +9 -3
  27. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/METADATA +1 -1
  28. relenv-0.22.0.dist-info/RECORD +48 -0
  29. tests/__init__.py +2 -0
  30. tests/_pytest_typing.py +45 -0
  31. tests/conftest.py +42 -36
  32. tests/test_build.py +426 -9
  33. tests/test_common.py +311 -48
  34. tests/test_create.py +149 -6
  35. tests/test_downloads.py +19 -15
  36. tests/test_fips_photon.py +6 -3
  37. tests/test_module_imports.py +44 -0
  38. tests/test_pyversions_runtime.py +177 -0
  39. tests/test_relocate.py +45 -39
  40. tests/test_relocate_module.py +257 -0
  41. tests/test_runtime.py +1802 -6
  42. tests/test_verify_build.py +477 -34
  43. relenv/build/common.py +0 -1707
  44. relenv-0.21.2.dist-info/RECORD +0 -35
  45. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/WHEEL +0 -0
  46. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/entry_points.txt +0 -0
  47. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/licenses/LICENSE.md +0 -0
  48. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/licenses/NOTICE +0 -0
  49. {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/top_level.txt +0 -0
tests/test_build.py CHANGED
@@ -1,27 +1,45 @@
1
1
  # Copyright 2022-2025 Broadcom.
2
- # SPDX-License-Identifier: Apache-2
2
+ # SPDX-License-Identifier: Apache-2.0
3
3
  import hashlib
4
+ import logging
5
+ import pathlib
4
6
 
5
7
  import pytest
6
8
 
7
- from relenv.build.common import Builder, verify_checksum
8
- from relenv.common import DATA_DIR, RelenvException
9
+ from relenv.build.common import Dirs, get_dependency_version
10
+ from relenv.build.common.builder import Builder
11
+ from relenv.build.common.download import Download, verify_checksum
12
+ from relenv.build.common.ui import (
13
+ BuildStats,
14
+ LineCountHandler,
15
+ load_build_stats,
16
+ save_build_stats,
17
+ update_build_stats,
18
+ )
19
+ from relenv.common import DATA_DIR, RelenvException, toolchain_root_dir, work_dirs
20
+
21
+ # mypy: ignore-errors
9
22
 
10
23
 
11
24
  @pytest.fixture
12
- def fake_download(tmp_path):
25
+ def fake_download(tmp_path: pathlib.Path) -> pathlib.Path:
13
26
  download = tmp_path / "fake_download"
14
27
  download.write_text("This is some file contents")
15
28
  return download
16
29
 
17
30
 
18
31
  @pytest.fixture
19
- def fake_download_md5(fake_download):
32
+ def fake_download_md5(fake_download: pathlib.Path) -> str:
20
33
  return hashlib.sha1(fake_download.read_bytes()).hexdigest()
21
34
 
22
35
 
36
+ @pytest.fixture
37
+ def fake_download_sha256(fake_download: pathlib.Path) -> str:
38
+ return hashlib.sha256(fake_download.read_bytes()).hexdigest()
39
+
40
+
23
41
  @pytest.mark.skip_unless_on_linux
24
- def test_builder_defaults_linux():
42
+ def test_builder_defaults_linux() -> None:
25
43
  builder = Builder(version="3.10.10")
26
44
  assert builder.arch == "x86_64"
27
45
  assert builder.arch == "x86_64"
@@ -29,15 +47,414 @@ def test_builder_defaults_linux():
29
47
  assert builder.prefix == DATA_DIR / "build" / "3.10.10-x86_64-linux-gnu"
30
48
  assert builder.sources == DATA_DIR / "src"
31
49
  assert builder.downloads == DATA_DIR / "download"
32
- assert "relenv/toolchain" in str(builder.toolchain)
50
+ assert builder.toolchain == toolchain_root_dir() / builder.triplet
33
51
  assert callable(builder.build_default)
34
52
  assert callable(builder.populate_env)
35
53
  assert builder.recipies == {}
36
54
 
37
55
 
38
- def test_verify_checksum(fake_download, fake_download_md5):
56
+ @pytest.mark.skip_unless_on_linux
57
+ def test_builder_toolchain_lazy_loading(monkeypatch: pytest.MonkeyPatch) -> None:
58
+ """Test that toolchain is only fetched when accessed (lazy loading)."""
59
+ import relenv.common
60
+
61
+ call_count = {"count": 0}
62
+
63
+ def mock_get_toolchain(arch=None, root=None):
64
+ call_count["count"] += 1
65
+ # Return a fake path instead of actually extracting
66
+ return pathlib.Path(f"/fake/toolchain/{arch or 'default'}")
67
+
68
+ # Patch where get_toolchain is actually imported and used (in relenv.common)
69
+ monkeypatch.setattr(relenv.common, "get_toolchain", mock_get_toolchain)
70
+
71
+ # Create builder - should NOT call get_toolchain yet
72
+ builder = Builder(version="3.10.10", arch="aarch64")
73
+ assert call_count["count"] == 0, "get_toolchain should not be called during init"
74
+
75
+ # Access toolchain property - should call get_toolchain once
76
+ toolchain = builder.toolchain
77
+ assert (
78
+ call_count["count"] == 1
79
+ ), "get_toolchain should be called when property is accessed"
80
+ assert toolchain == pathlib.Path("/fake/toolchain/aarch64")
81
+
82
+ # Access again - should use cached value, not call again
83
+ toolchain2 = builder.toolchain
84
+ assert call_count["count"] == 1, "get_toolchain should only be called once (cached)"
85
+ assert toolchain == toolchain2
86
+
87
+ # Change arch - should reset cache
88
+ builder.set_arch("x86_64")
89
+ assert builder._toolchain is None, "Changing arch should reset toolchain cache"
90
+
91
+ # Access after arch change - should call get_toolchain again
92
+ toolchain3 = builder.toolchain
93
+ assert (
94
+ call_count["count"] == 2
95
+ ), "get_toolchain should be called again after arch change"
96
+ assert toolchain3 == pathlib.Path("/fake/toolchain/x86_64")
97
+
98
+
99
+ def test_verify_checksum(fake_download: pathlib.Path, fake_download_md5: str) -> None:
39
100
  assert verify_checksum(fake_download, fake_download_md5) is True
40
101
 
41
102
 
42
- def test_verify_checksum_failed(fake_download):
103
+ def test_verify_checksum_sha256(
104
+ fake_download: pathlib.Path, fake_download_sha256: str
105
+ ) -> None:
106
+ """Test SHA-256 checksum validation."""
107
+ assert verify_checksum(fake_download, fake_download_sha256) is True
108
+
109
+
110
+ def test_verify_checksum_failed(fake_download: pathlib.Path) -> None:
43
111
  pytest.raises(RelenvException, verify_checksum, fake_download, "no")
112
+
113
+
114
+ def test_verify_checksum_none(fake_download: pathlib.Path) -> None:
115
+ """Test that verify_checksum returns False when checksum is None."""
116
+ assert verify_checksum(fake_download, None) is False
117
+
118
+
119
+ def test_verify_checksum_invalid_length(fake_download: pathlib.Path) -> None:
120
+ """Test that invalid checksum length raises error."""
121
+ with pytest.raises(RelenvException, match="Invalid checksum length"):
122
+ verify_checksum(fake_download, "abc123") # 6 chars, not 40 or 64
123
+
124
+
125
+ def test_get_dependency_version_openssl_linux() -> None:
126
+ """Test getting OpenSSL version for Linux platform."""
127
+ result = get_dependency_version("openssl", "linux")
128
+ assert result is not None
129
+ assert isinstance(result, dict)
130
+ assert "version" in result
131
+ assert "url" in result
132
+ assert "sha256" in result
133
+ assert isinstance(result["version"], str)
134
+ assert "openssl" in result["url"].lower()
135
+ assert "{version}" in result["url"]
136
+ assert isinstance(result["sha256"], str)
137
+
138
+
139
+ def test_get_dependency_version_sqlite_all_platforms() -> None:
140
+ """Test getting SQLite version for various platforms."""
141
+ for platform in ["linux", "darwin", "win32"]:
142
+ result = get_dependency_version("sqlite", platform)
143
+ assert result is not None, f"SQLite should be available for {platform}"
144
+ assert isinstance(result, dict)
145
+ assert "version" in result
146
+ assert "url" in result
147
+ assert "sha256" in result
148
+ assert "sqliteversion" in result, "SQLite should have sqliteversion field"
149
+ assert isinstance(result["version"], str)
150
+ assert "sqlite" in result["url"].lower()
151
+ assert isinstance(result["sha256"], str)
152
+
153
+
154
+ def test_get_dependency_version_xz_all_platforms() -> None:
155
+ """Test getting XZ version for various platforms."""
156
+ # XZ 5.5.0+ removed MSBuild support, so Windows uses a fallback version
157
+ # and XZ is not in JSON for win32
158
+ for platform in ["linux", "darwin"]:
159
+ result = get_dependency_version("xz", platform)
160
+ assert result is not None, f"XZ should be available for {platform}"
161
+ assert isinstance(result, dict)
162
+ assert "version" in result
163
+ assert "url" in result
164
+ assert "sha256" in result
165
+ assert isinstance(result["version"], str)
166
+ assert "xz" in result["url"].lower()
167
+ assert isinstance(result["sha256"], str)
168
+
169
+ # Windows should return None (uses hardcoded fallback in windows.py)
170
+ result = get_dependency_version("xz", "win32")
171
+ assert result is None, "XZ should not be in JSON for win32 (uses fallback)"
172
+
173
+
174
+ def test_get_dependency_version_nonexistent() -> None:
175
+ """Test that nonexistent dependency returns None."""
176
+ result = get_dependency_version("nonexistent-dep", "linux")
177
+ assert result is None
178
+
179
+
180
+ def test_get_dependency_version_wrong_platform() -> None:
181
+ """Test that requesting unsupported platform returns None."""
182
+ # Try to get OpenSSL for a platform that doesn't exist
183
+ result = get_dependency_version("openssl", "nonexistent-platform")
184
+ assert result is None
185
+
186
+
187
+ # Build stats tests
188
+
189
+
190
+ def test_build_stats_save_load(
191
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
192
+ ) -> None:
193
+ """Test saving and loading build statistics."""
194
+ monkeypatch.setattr("relenv.build.common.ui.DATA_DIR", tmp_path)
195
+
196
+ # Save some stats
197
+ stats = {
198
+ "python": BuildStats(avg_lines=100, samples=1, last_lines=100),
199
+ "openssl": BuildStats(avg_lines=200, samples=2, last_lines=180),
200
+ }
201
+ save_build_stats(stats)
202
+
203
+ # Load them back
204
+ loaded = load_build_stats()
205
+ assert loaded["python"]["avg_lines"] == 100
206
+ assert loaded["python"]["samples"] == 1
207
+ assert loaded["python"]["last_lines"] == 100
208
+ assert loaded["openssl"]["avg_lines"] == 200
209
+ assert loaded["openssl"]["samples"] == 2
210
+
211
+
212
+ def test_build_stats_load_nonexistent(
213
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
214
+ ) -> None:
215
+ """Test loading stats when file doesn't exist returns empty dict."""
216
+ monkeypatch.setattr("relenv.build.common.ui.DATA_DIR", tmp_path)
217
+ loaded = load_build_stats()
218
+ assert loaded == {}
219
+
220
+
221
+ def test_build_stats_update_new_step(
222
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
223
+ ) -> None:
224
+ """Test updating stats for a new build step."""
225
+ monkeypatch.setattr("relenv.build.common.ui.DATA_DIR", tmp_path)
226
+
227
+ # Update a new step
228
+ update_build_stats("python", 100)
229
+
230
+ # Load and verify
231
+ stats = load_build_stats()
232
+ assert stats["python"]["avg_lines"] == 100
233
+ assert stats["python"]["samples"] == 1
234
+ assert stats["python"]["last_lines"] == 100
235
+
236
+
237
+ def test_build_stats_update_existing_step(
238
+ tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
239
+ ) -> None:
240
+ """Test updating stats for an existing step uses exponential moving average."""
241
+ monkeypatch.setattr("relenv.build.common.ui.DATA_DIR", tmp_path)
242
+
243
+ # Initial value
244
+ update_build_stats("python", 100)
245
+
246
+ # Update with new value
247
+ update_build_stats("python", 200)
248
+
249
+ # Load and verify exponential moving average: 0.7 * 200 + 0.3 * 100 = 170
250
+ stats = load_build_stats()
251
+ assert stats["python"]["avg_lines"] == 170
252
+ assert stats["python"]["samples"] == 2
253
+ assert stats["python"]["last_lines"] == 200
254
+
255
+
256
+ # LineCountHandler tests
257
+
258
+
259
+ def test_line_count_handler() -> None:
260
+ """Test LineCountHandler increments shared dict correctly."""
261
+ shared_dict = {}
262
+ handler = LineCountHandler("test", shared_dict)
263
+
264
+ # Create a log record
265
+ record = logging.LogRecord(
266
+ name="test",
267
+ level=logging.INFO,
268
+ pathname="",
269
+ lineno=0,
270
+ msg="test message",
271
+ args=(),
272
+ exc_info=None,
273
+ )
274
+
275
+ # Emit first record
276
+ handler.emit(record)
277
+ assert shared_dict["test"] == 1
278
+
279
+ # Emit second record
280
+ handler.emit(record)
281
+ assert shared_dict["test"] == 2
282
+
283
+ # Emit third record
284
+ handler.emit(record)
285
+ assert shared_dict["test"] == 3
286
+
287
+
288
+ def test_line_count_handler_multiple_steps() -> None:
289
+ """Test LineCountHandler tracks multiple steps independently."""
290
+ shared_dict = {}
291
+ handler1 = LineCountHandler("step1", shared_dict)
292
+ handler2 = LineCountHandler("step2", shared_dict)
293
+
294
+ record = logging.LogRecord(
295
+ name="test",
296
+ level=logging.INFO,
297
+ pathname="",
298
+ lineno=0,
299
+ msg="test",
300
+ args=(),
301
+ exc_info=None,
302
+ )
303
+
304
+ handler1.emit(record)
305
+ handler1.emit(record)
306
+ handler2.emit(record)
307
+
308
+ assert shared_dict["step1"] == 2
309
+ assert shared_dict["step2"] == 1
310
+
311
+
312
+ # Dirs class tests
313
+
314
+
315
+ @pytest.mark.skip_unless_on_linux
316
+ def test_dirs_initialization() -> None:
317
+ """Test Dirs class initialization."""
318
+ dirs = Dirs(work_dirs(), "python", "x86_64", "3.10.0")
319
+ assert dirs.name == "python"
320
+ assert dirs.arch == "x86_64"
321
+ assert dirs.version == "3.10.0"
322
+ assert "python_build" in dirs.tmpbuild
323
+
324
+
325
+ def test_dirs_triplet_darwin(monkeypatch: pytest.MonkeyPatch) -> None:
326
+ """Test Dirs._triplet property for darwin platform."""
327
+ monkeypatch.setattr("sys.platform", "darwin")
328
+ dirs = Dirs(work_dirs(), "test", "arm64", "3.10.0")
329
+ assert dirs._triplet == "arm64-macos"
330
+
331
+
332
+ def test_dirs_triplet_win32(monkeypatch: pytest.MonkeyPatch) -> None:
333
+ """Test Dirs._triplet property for win32 platform."""
334
+ monkeypatch.setattr("sys.platform", "win32")
335
+ dirs = Dirs(work_dirs(), "test", "amd64", "3.10.0")
336
+ assert dirs._triplet == "amd64-win"
337
+
338
+
339
+ @pytest.mark.skip_unless_on_linux
340
+ def test_dirs_triplet_linux() -> None:
341
+ """Test Dirs._triplet property for linux platform."""
342
+ dirs = Dirs(work_dirs(), "test", "x86_64", "3.10.0")
343
+ assert dirs._triplet == "x86_64-linux-gnu"
344
+
345
+
346
+ @pytest.mark.skip_unless_on_linux
347
+ def test_dirs_prefix() -> None:
348
+ """Test Dirs.prefix property."""
349
+ dirs = Dirs(work_dirs(), "test", "x86_64", "3.10.0")
350
+ assert "3.10.0-x86_64-linux-gnu" in str(dirs.prefix)
351
+
352
+
353
+ @pytest.mark.skip_unless_on_linux
354
+ def test_dirs_to_dict() -> None:
355
+ """Test Dirs.to_dict() method."""
356
+ dirs = Dirs(work_dirs(), "test", "x86_64", "3.10.0")
357
+ d = dirs.to_dict()
358
+ assert "root" in d
359
+ assert "prefix" in d
360
+ assert "downloads" in d
361
+ assert "logs" in d
362
+ assert "sources" in d
363
+ assert "build" in d
364
+ assert "toolchain" in d
365
+
366
+
367
+ @pytest.mark.skip_unless_on_linux
368
+ def test_dirs_pickle() -> None:
369
+ """Test Dirs serialization/deserialization."""
370
+ dirs = Dirs(work_dirs(), "python", "x86_64", "3.10.0")
371
+
372
+ # Get state
373
+ state = dirs.__getstate__()
374
+ assert state["name"] == "python"
375
+ assert state["arch"] == "x86_64"
376
+
377
+ # Create new instance and restore state
378
+ dirs2 = Dirs.__new__(Dirs)
379
+ dirs2.__setstate__(state)
380
+ assert dirs2.name == "python"
381
+ assert dirs2.arch == "x86_64"
382
+ assert dirs2.tmpbuild == dirs.tmpbuild
383
+
384
+
385
+ # Download class tests
386
+
387
+
388
+ def test_download_copy() -> None:
389
+ """Test Download.copy() creates independent copy."""
390
+ d1 = Download(
391
+ "test",
392
+ "http://example.com/{version}/test.tar.gz",
393
+ version="1.0.0",
394
+ checksum="abc123",
395
+ )
396
+ d2 = d1.copy()
397
+
398
+ # Verify copy has same values
399
+ assert d2.name == d1.name
400
+ assert d2.url_tpl == d1.url_tpl
401
+ assert d2.version == d1.version
402
+ assert d2.checksum == d1.checksum
403
+
404
+ # Verify it's a different object
405
+ assert d2 is not d1
406
+
407
+ # Verify modifying copy doesn't affect original
408
+ d2.version = "2.0.0"
409
+ assert d1.version == "1.0.0"
410
+ assert d2.version == "2.0.0"
411
+
412
+
413
+ def test_download_fallback_url() -> None:
414
+ """Test Download.fallback_url property."""
415
+ d = Download(
416
+ "test",
417
+ "http://main.com/{version}/test.tar.gz",
418
+ fallback_url="http://backup.com/{version}/test.tar.gz",
419
+ version="1.0.0",
420
+ )
421
+ assert d.fallback_url == "http://backup.com/1.0.0/test.tar.gz"
422
+
423
+
424
+ def test_download_no_fallback() -> None:
425
+ """Test Download.fallback_url returns None when not configured."""
426
+ d = Download("test", "http://example.com/{version}/test.tar.gz", version="1.0.0")
427
+ assert d.fallback_url is None
428
+
429
+
430
+ def test_download_signature_url() -> None:
431
+ """Test Download.signature_url property."""
432
+ d = Download(
433
+ "test",
434
+ "http://example.com/{version}/test.tar.gz",
435
+ signature="http://example.com/{version}/test.tar.gz.asc",
436
+ version="1.0.0",
437
+ )
438
+ assert d.signature_url == "http://example.com/1.0.0/test.tar.gz.asc"
439
+
440
+
441
+ def test_download_signature_url_error() -> None:
442
+ """Test Download.signature_url raises error when not configured."""
443
+ from relenv.common import ConfigurationError
444
+
445
+ d = Download("test", "http://example.com/test.tar.gz")
446
+ with pytest.raises(ConfigurationError, match="Signature template not configured"):
447
+ _ = d.signature_url
448
+
449
+
450
+ def test_download_destination_setter() -> None:
451
+ """Test Download.destination setter with None value."""
452
+ d = Download("test", "http://example.com/test.tar.gz")
453
+
454
+ # Set to a path
455
+ d.destination = "/tmp/downloads"
456
+ assert d.destination == pathlib.Path("/tmp/downloads")
457
+
458
+ # Set to None
459
+ d.destination = None
460
+ assert d.destination == pathlib.Path()