gira-cli 1.0.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.
- gira_cli-1.0.0/PKG-INFO +25 -0
- gira_cli-1.0.0/README.md +12 -0
- gira_cli-1.0.0/pyproject.toml +29 -0
- gira_cli-1.0.0/setup.cfg +4 -0
- gira_cli-1.0.0/src/gira_cli/__init__.py +3 -0
- gira_cli-1.0.0/src/gira_cli/installer.py +229 -0
- gira_cli-1.0.0/src/gira_cli.egg-info/PKG-INFO +25 -0
- gira_cli-1.0.0/src/gira_cli.egg-info/SOURCES.txt +10 -0
- gira_cli-1.0.0/src/gira_cli.egg-info/dependency_links.txt +1 -0
- gira_cli-1.0.0/src/gira_cli.egg-info/entry_points.txt +2 -0
- gira_cli-1.0.0/src/gira_cli.egg-info/top_level.txt +1 -0
- gira_cli-1.0.0/tests/test_installer.py +123 -0
gira_cli-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gira-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Jira-style project flow on GitHub, distributed as the official Go-built gira binary.
|
|
5
|
+
Author: StatPan
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Environment :: Console
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# gira-cli
|
|
15
|
+
|
|
16
|
+
PyPI distribution wrapper for the official Go-built `gira` binary.
|
|
17
|
+
|
|
18
|
+
This package does not reimplement Gira in Python. The `gira` console command downloads the matching GitHub Release archive on first run, verifies `checksums.txt`, caches the native binary, and then executes it.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install gira-cli
|
|
22
|
+
gira version
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The PyPI package name is `gira-cli` because the `gira` package name is already occupied on PyPI. The installed command is still `gira`.
|
gira_cli-1.0.0/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# gira-cli
|
|
2
|
+
|
|
3
|
+
PyPI distribution wrapper for the official Go-built `gira` binary.
|
|
4
|
+
|
|
5
|
+
This package does not reimplement Gira in Python. The `gira` console command downloads the matching GitHub Release archive on first run, verifies `checksums.txt`, caches the native binary, and then executes it.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install gira-cli
|
|
9
|
+
gira version
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The PyPI package name is `gira-cli` because the `gira` package name is already occupied on PyPI. The installed command is still `gira`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gira-cli"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Jira-style project flow on GitHub, distributed as the official Go-built gira binary."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "StatPan" }
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
gira = "gira_cli:main"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
package-dir = { "" = "src" }
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
gira_cli-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import importlib.metadata
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import stat
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tarfile
|
|
12
|
+
import tempfile
|
|
13
|
+
import urllib.request
|
|
14
|
+
import zipfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import BinaryIO, Dict, Optional
|
|
17
|
+
|
|
18
|
+
REPO = "StatPan/gira"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def package_version() -> str:
|
|
22
|
+
return importlib.metadata.version("gira-cli")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def release_version() -> str:
|
|
26
|
+
env_version = os.environ.get("GIRA_VERSION", "").strip()
|
|
27
|
+
if env_version:
|
|
28
|
+
return env_version
|
|
29
|
+
version = package_version()
|
|
30
|
+
if "dev" in version:
|
|
31
|
+
return ""
|
|
32
|
+
return version if version.startswith("v") else f"v{version}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_platform(system: Optional[str] = None, machine: Optional[str] = None) -> Dict[str, str]:
|
|
36
|
+
system = (system or platform.system()).lower()
|
|
37
|
+
machine = (machine or platform.machine()).lower()
|
|
38
|
+
os_map = {
|
|
39
|
+
"linux": "linux",
|
|
40
|
+
"darwin": "darwin",
|
|
41
|
+
"windows": "windows",
|
|
42
|
+
}
|
|
43
|
+
arch_map = {
|
|
44
|
+
"x86_64": "amd64",
|
|
45
|
+
"amd64": "amd64",
|
|
46
|
+
"arm64": "arm64",
|
|
47
|
+
"aarch64": "arm64",
|
|
48
|
+
}
|
|
49
|
+
goos = os_map.get(system)
|
|
50
|
+
goarch = arch_map.get(machine)
|
|
51
|
+
if not goos:
|
|
52
|
+
raise RuntimeError(f"unsupported OS: {system}")
|
|
53
|
+
if not goarch:
|
|
54
|
+
raise RuntimeError(f"unsupported architecture: {machine}")
|
|
55
|
+
if goos == "windows" and goarch != "amd64":
|
|
56
|
+
raise RuntimeError(f"unsupported Windows architecture: {machine}")
|
|
57
|
+
return {
|
|
58
|
+
"goos": goos,
|
|
59
|
+
"goarch": goarch,
|
|
60
|
+
"extension": "zip" if goos == "windows" else "tar.gz",
|
|
61
|
+
"binary": "gira.exe" if goos == "windows" else "gira",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def archive_name(version: str, info: Dict[str, str]) -> str:
|
|
66
|
+
return f"gira_{version}_{info['goos']}_{info['goarch']}.{info['extension']}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def base_url(version: str) -> str:
|
|
70
|
+
return os.environ.get("GIRA_BASE_URL", "").strip() or f"https://github.com/{REPO}/releases/download/{version}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cache_dir(version: str, info: Dict[str, str]) -> Path:
|
|
74
|
+
override = os.environ.get("GIRA_PYPI_CACHE_DIR", "").strip()
|
|
75
|
+
root = Path(override).expanduser() if override else Path.home() / ".cache" / "gira-cli"
|
|
76
|
+
return root / version / f"{info['goos']}_{info['goarch']}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def binary_checksum_path(target: Path) -> Path:
|
|
80
|
+
return target.with_name(f"{target.name}.sha256")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def file_checksum(path: Path) -> str:
|
|
84
|
+
digest = hashlib.sha256()
|
|
85
|
+
with path.open("rb") as source:
|
|
86
|
+
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
87
|
+
digest.update(chunk)
|
|
88
|
+
return digest.hexdigest()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def cached_binary_valid(target: Path) -> bool:
|
|
92
|
+
marker = binary_checksum_path(target)
|
|
93
|
+
if not target.is_file() or not marker.is_file():
|
|
94
|
+
return False
|
|
95
|
+
if os.name != "nt" and not os.access(target, os.X_OK):
|
|
96
|
+
return False
|
|
97
|
+
return marker.read_text().strip() == file_checksum(target)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def read_url(url: str) -> bytes:
|
|
101
|
+
if url.startswith("file://"):
|
|
102
|
+
return Path(urllib.request.url2pathname(url[7:])).read_bytes()
|
|
103
|
+
with urllib.request.urlopen(url) as response:
|
|
104
|
+
return response.read()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def download_file(url: str, target: Path) -> None:
|
|
108
|
+
target.write_bytes(read_url(url))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def checksum_for(checksums: str, archive: str) -> str:
|
|
112
|
+
for line in checksums.splitlines():
|
|
113
|
+
parts = line.strip().split()
|
|
114
|
+
if len(parts) >= 2 and parts[1] in {archive, f"*{archive}"}:
|
|
115
|
+
return parts[0]
|
|
116
|
+
raise RuntimeError(f"checksum asset does not include {archive}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def verify_checksum(path: Path, expected: str) -> None:
|
|
120
|
+
actual = file_checksum(path)
|
|
121
|
+
if actual != expected:
|
|
122
|
+
raise RuntimeError(f"checksum mismatch for {path.name}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_archive_member(name: str) -> Path:
|
|
126
|
+
if not name or "\x00" in name or "\\" in name or ":" in name:
|
|
127
|
+
raise RuntimeError(f"unsafe archive member path: {name}")
|
|
128
|
+
path = Path(name)
|
|
129
|
+
if path.is_absolute() or ".." in path.parts:
|
|
130
|
+
raise RuntimeError(f"unsafe archive member path: {name}")
|
|
131
|
+
return path
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def copy_member(source: BinaryIO, target: Path) -> None:
|
|
135
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
with target.open("wb") as output:
|
|
137
|
+
shutil.copyfileobj(source, output)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def extract_archive(path: Path, extension: str, output_dir: Path) -> None:
|
|
141
|
+
if extension == "zip":
|
|
142
|
+
with zipfile.ZipFile(path) as archive:
|
|
143
|
+
for member in archive.infolist():
|
|
144
|
+
target = output_dir / validate_archive_member(member.filename)
|
|
145
|
+
if member.is_dir():
|
|
146
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
continue
|
|
148
|
+
with archive.open(member) as source:
|
|
149
|
+
copy_member(source, target)
|
|
150
|
+
return
|
|
151
|
+
with tarfile.open(path, "r:gz") as archive:
|
|
152
|
+
for member in archive.getmembers():
|
|
153
|
+
target = output_dir / validate_archive_member(member.name)
|
|
154
|
+
if member.isdir():
|
|
155
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
continue
|
|
157
|
+
if not member.isfile():
|
|
158
|
+
raise RuntimeError(f"unsupported archive member type: {member.name}")
|
|
159
|
+
source = archive.extractfile(member)
|
|
160
|
+
if source is None:
|
|
161
|
+
raise RuntimeError(f"unable to extract archive member: {member.name}")
|
|
162
|
+
with source:
|
|
163
|
+
copy_member(source, target)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def install_cached_binary(source: Path, target: Path) -> None:
|
|
167
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
checksum = file_checksum(source)
|
|
169
|
+
with tempfile.NamedTemporaryFile(prefix=f".{target.name}.", dir=target.parent, delete=False) as tmp:
|
|
170
|
+
tmp_path = Path(tmp.name)
|
|
171
|
+
marker_tmp_path: Optional[Path] = None
|
|
172
|
+
try:
|
|
173
|
+
shutil.copy2(source, tmp_path)
|
|
174
|
+
tmp_path.chmod(tmp_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
175
|
+
os.replace(tmp_path, target)
|
|
176
|
+
|
|
177
|
+
marker = binary_checksum_path(target)
|
|
178
|
+
with tempfile.NamedTemporaryFile(
|
|
179
|
+
mode="w", prefix=f".{marker.name}.", dir=target.parent, delete=False
|
|
180
|
+
) as marker_tmp:
|
|
181
|
+
marker_tmp.write(f"{checksum}\n")
|
|
182
|
+
marker_tmp_path = Path(marker_tmp.name)
|
|
183
|
+
os.replace(marker_tmp_path, marker)
|
|
184
|
+
finally:
|
|
185
|
+
tmp_path.unlink(missing_ok=True)
|
|
186
|
+
if marker_tmp_path is not None:
|
|
187
|
+
marker_tmp_path.unlink(missing_ok=True)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def ensure_binary() -> Path:
|
|
191
|
+
version = release_version()
|
|
192
|
+
if not version:
|
|
193
|
+
raise RuntimeError("gira-cli development package cannot resolve a release version; set GIRA_VERSION")
|
|
194
|
+
info = resolve_platform()
|
|
195
|
+
target_dir = cache_dir(version, info)
|
|
196
|
+
target = target_dir / info["binary"]
|
|
197
|
+
if cached_binary_valid(target):
|
|
198
|
+
return target
|
|
199
|
+
|
|
200
|
+
archive = archive_name(version, info)
|
|
201
|
+
root = base_url(version)
|
|
202
|
+
with tempfile.TemporaryDirectory(prefix="gira-pypi-") as tmp:
|
|
203
|
+
tmpdir = Path(tmp)
|
|
204
|
+
archive_path = tmpdir / archive
|
|
205
|
+
checksums_path = tmpdir / "checksums.txt"
|
|
206
|
+
download_file(f"{root}/{archive}", archive_path)
|
|
207
|
+
download_file(f"{root}/checksums.txt", checksums_path)
|
|
208
|
+
expected = checksum_for(checksums_path.read_text(), archive)
|
|
209
|
+
verify_checksum(archive_path, expected)
|
|
210
|
+
extract_archive(archive_path, info["extension"], tmpdir)
|
|
211
|
+
extracted = tmpdir / f"gira_{version}_{info['goos']}_{info['goarch']}" / info["binary"]
|
|
212
|
+
if not extracted.exists():
|
|
213
|
+
raise RuntimeError(f"release archive did not contain {info['binary']}")
|
|
214
|
+
install_cached_binary(extracted, target)
|
|
215
|
+
return target
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main() -> int:
|
|
219
|
+
try:
|
|
220
|
+
binary = ensure_binary()
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
print(f"gira pip wrapper: {exc}", file=sys.stderr)
|
|
223
|
+
return 1
|
|
224
|
+
completed = subprocess.run([str(binary), *sys.argv[1:]], check=False)
|
|
225
|
+
return int(completed.returncode)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gira-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Jira-style project flow on GitHub, distributed as the official Go-built gira binary.
|
|
5
|
+
Author: StatPan
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Environment :: Console
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# gira-cli
|
|
15
|
+
|
|
16
|
+
PyPI distribution wrapper for the official Go-built `gira` binary.
|
|
17
|
+
|
|
18
|
+
This package does not reimplement Gira in Python. The `gira` console command downloads the matching GitHub Release archive on first run, verifies `checksums.txt`, caches the native binary, and then executes it.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install gira-cli
|
|
22
|
+
gira version
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The PyPI package name is `gira-cli` because the `gira` package name is already occupied on PyPI. The installed command is still `gira`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/gira_cli/__init__.py
|
|
4
|
+
src/gira_cli/installer.py
|
|
5
|
+
src/gira_cli.egg-info/PKG-INFO
|
|
6
|
+
src/gira_cli.egg-info/SOURCES.txt
|
|
7
|
+
src/gira_cli.egg-info/dependency_links.txt
|
|
8
|
+
src/gira_cli.egg-info/entry_points.txt
|
|
9
|
+
src/gira_cli.egg-info/top_level.txt
|
|
10
|
+
tests/test_installer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gira_cli
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
import tarfile
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest import mock
|
|
9
|
+
|
|
10
|
+
from gira_cli import installer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InstallerTest(unittest.TestCase):
|
|
14
|
+
def test_archive_names_match_release_assets(self):
|
|
15
|
+
cases = [
|
|
16
|
+
("Linux", "x86_64", "gira_v1.2.3_linux_amd64.tar.gz"),
|
|
17
|
+
("Linux", "aarch64", "gira_v1.2.3_linux_arm64.tar.gz"),
|
|
18
|
+
("Darwin", "x86_64", "gira_v1.2.3_darwin_amd64.tar.gz"),
|
|
19
|
+
("Darwin", "arm64", "gira_v1.2.3_darwin_arm64.tar.gz"),
|
|
20
|
+
("Windows", "AMD64", "gira_v1.2.3_windows_amd64.zip"),
|
|
21
|
+
]
|
|
22
|
+
for system, machine, expected in cases:
|
|
23
|
+
with self.subTest(system=system, machine=machine):
|
|
24
|
+
info = installer.resolve_platform(system, machine)
|
|
25
|
+
self.assertEqual(installer.archive_name("v1.2.3", info), expected)
|
|
26
|
+
|
|
27
|
+
def test_rejects_unsupported_platforms(self):
|
|
28
|
+
with self.assertRaisesRegex(RuntimeError, "unsupported OS"):
|
|
29
|
+
installer.resolve_platform("FreeBSD", "x86_64")
|
|
30
|
+
with self.assertRaisesRegex(RuntimeError, "unsupported architecture"):
|
|
31
|
+
installer.resolve_platform("Linux", "i386")
|
|
32
|
+
with self.assertRaisesRegex(RuntimeError, "unsupported Windows architecture"):
|
|
33
|
+
installer.resolve_platform("Windows", "arm64")
|
|
34
|
+
|
|
35
|
+
def test_reads_checksum_entry_for_archive(self):
|
|
36
|
+
checksums = "\n".join(
|
|
37
|
+
[
|
|
38
|
+
"abc123 gira_v1.2.3_linux_amd64.tar.gz",
|
|
39
|
+
"def456 *gira_v1.2.3_darwin_arm64.tar.gz",
|
|
40
|
+
]
|
|
41
|
+
)
|
|
42
|
+
self.assertEqual(installer.checksum_for(checksums, "gira_v1.2.3_linux_amd64.tar.gz"), "abc123")
|
|
43
|
+
self.assertEqual(installer.checksum_for(checksums, "gira_v1.2.3_darwin_arm64.tar.gz"), "def456")
|
|
44
|
+
with self.assertRaisesRegex(RuntimeError, "does not include"):
|
|
45
|
+
installer.checksum_for(checksums, "missing.tar.gz")
|
|
46
|
+
|
|
47
|
+
def test_checksum_mismatch_fails_closed(self):
|
|
48
|
+
with tempfile.TemporaryDirectory(prefix="gira-pypi-test-") as tmp:
|
|
49
|
+
archive = Path(tmp) / "archive.tar.gz"
|
|
50
|
+
archive.write_text("hello")
|
|
51
|
+
with self.assertRaisesRegex(RuntimeError, "checksum mismatch"):
|
|
52
|
+
installer.verify_checksum(archive, "0000")
|
|
53
|
+
|
|
54
|
+
def test_checksum_match_passes(self):
|
|
55
|
+
with tempfile.TemporaryDirectory(prefix="gira-pypi-test-") as tmp:
|
|
56
|
+
archive = Path(tmp) / "archive.tar.gz"
|
|
57
|
+
archive.write_text("hello")
|
|
58
|
+
expected = hashlib.sha256(b"hello").hexdigest()
|
|
59
|
+
installer.verify_checksum(archive, expected)
|
|
60
|
+
|
|
61
|
+
def test_release_version_can_be_forced_by_environment(self):
|
|
62
|
+
previous = os.environ.get("GIRA_VERSION")
|
|
63
|
+
os.environ["GIRA_VERSION"] = "v9.8.7"
|
|
64
|
+
try:
|
|
65
|
+
self.assertEqual(installer.release_version(), "v9.8.7")
|
|
66
|
+
finally:
|
|
67
|
+
if previous is None:
|
|
68
|
+
os.environ.pop("GIRA_VERSION", None)
|
|
69
|
+
else:
|
|
70
|
+
os.environ["GIRA_VERSION"] = previous
|
|
71
|
+
|
|
72
|
+
def test_release_version_maps_package_version_to_tag(self):
|
|
73
|
+
previous = os.environ.pop("GIRA_VERSION", None)
|
|
74
|
+
try:
|
|
75
|
+
with mock.patch.object(installer, "package_version", return_value="1.2.3"):
|
|
76
|
+
self.assertEqual(installer.release_version(), "v1.2.3")
|
|
77
|
+
with mock.patch.object(installer, "package_version", return_value="v1.2.3"):
|
|
78
|
+
self.assertEqual(installer.release_version(), "v1.2.3")
|
|
79
|
+
with mock.patch.object(installer, "package_version", return_value="0.0.0.dev0"):
|
|
80
|
+
self.assertEqual(installer.release_version(), "")
|
|
81
|
+
finally:
|
|
82
|
+
if previous is not None:
|
|
83
|
+
os.environ["GIRA_VERSION"] = previous
|
|
84
|
+
|
|
85
|
+
def test_rejects_zip_path_traversal(self):
|
|
86
|
+
with tempfile.TemporaryDirectory(prefix="gira-pypi-test-") as tmp:
|
|
87
|
+
root = Path(tmp)
|
|
88
|
+
archive = root / "bad.zip"
|
|
89
|
+
with zipfile.ZipFile(archive, "w") as zip_file:
|
|
90
|
+
zip_file.writestr("../outside", "bad")
|
|
91
|
+
with self.assertRaisesRegex(RuntimeError, "unsafe archive member path"):
|
|
92
|
+
installer.extract_archive(archive, "zip", root / "out")
|
|
93
|
+
self.assertFalse((root / "outside").exists())
|
|
94
|
+
|
|
95
|
+
def test_rejects_tar_path_traversal(self):
|
|
96
|
+
with tempfile.TemporaryDirectory(prefix="gira-pypi-test-") as tmp:
|
|
97
|
+
root = Path(tmp)
|
|
98
|
+
archive = root / "bad.tar.gz"
|
|
99
|
+
payload = root / "payload"
|
|
100
|
+
payload.write_text("bad")
|
|
101
|
+
with tarfile.open(archive, "w:gz") as tar_file:
|
|
102
|
+
tar_file.add(payload, arcname="../outside")
|
|
103
|
+
with self.assertRaisesRegex(RuntimeError, "unsafe archive member path"):
|
|
104
|
+
installer.extract_archive(archive, "tar.gz", root / "out")
|
|
105
|
+
self.assertFalse((root / "outside").exists())
|
|
106
|
+
|
|
107
|
+
def test_cached_binary_requires_checksum_marker(self):
|
|
108
|
+
with tempfile.TemporaryDirectory(prefix="gira-pypi-test-") as tmp:
|
|
109
|
+
target = Path(tmp) / "gira"
|
|
110
|
+
target.write_text("hello")
|
|
111
|
+
target.chmod(0o755)
|
|
112
|
+
self.assertFalse(installer.cached_binary_valid(target))
|
|
113
|
+
|
|
114
|
+
installer.binary_checksum_path(target).write_text(f"{installer.file_checksum(target)}\n")
|
|
115
|
+
self.assertTrue(installer.cached_binary_valid(target))
|
|
116
|
+
|
|
117
|
+
target.write_text("corrupt")
|
|
118
|
+
target.chmod(0o755)
|
|
119
|
+
self.assertFalse(installer.cached_binary_valid(target))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
unittest.main()
|