ifchange 0.1.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.
- ifchange-0.1.0/.gitignore +4 -0
- ifchange-0.1.0/PKG-INFO +14 -0
- ifchange-0.1.0/ifchange/__init__.py +3 -0
- ifchange-0.1.0/ifchange/__main__.py +20 -0
- ifchange-0.1.0/ifchange/install.py +119 -0
- ifchange-0.1.0/pyproject.toml +28 -0
ifchange-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ifchange
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A fast linter for enforcing conditional change directives in source code
|
|
5
|
+
Project-URL: Homepage, https://github.com/slnc/ifchange
|
|
6
|
+
Project-URL: Repository, https://github.com/slnc/ifchange
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Requires-Python: >=3.8
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Entry point for ifchange."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .install import download_binary
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
bin_path = str(download_binary())
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
import subprocess
|
|
13
|
+
result = subprocess.run([bin_path] + sys.argv[1:])
|
|
14
|
+
sys.exit(result.returncode)
|
|
15
|
+
else:
|
|
16
|
+
os.execvp(bin_path, [bin_path] + sys.argv[1:])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Download and cache the ifchange binary from GitHub releases."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import stat
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import tempfile
|
|
11
|
+
import urllib.request
|
|
12
|
+
import zipfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
|
|
17
|
+
REPO = "slnc/ifchange"
|
|
18
|
+
BINARY = "ifchange"
|
|
19
|
+
|
|
20
|
+
PLATFORM_MAP = {
|
|
21
|
+
("Linux", "x86_64"): "x86_64-unknown-linux-gnu",
|
|
22
|
+
("Linux", "aarch64"): "aarch64-unknown-linux-gnu",
|
|
23
|
+
("Darwin", "x86_64"): "x86_64-apple-darwin",
|
|
24
|
+
("Darwin", "arm64"): "aarch64-apple-darwin",
|
|
25
|
+
("Windows", "AMD64"): "x86_64-pc-windows-msvc",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_target():
|
|
30
|
+
system = platform.system()
|
|
31
|
+
machine = platform.machine()
|
|
32
|
+
target = PLATFORM_MAP.get((system, machine))
|
|
33
|
+
if not target:
|
|
34
|
+
raise RuntimeError(f"Unsupported platform: {system} {machine}")
|
|
35
|
+
return target
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_bin_dir():
|
|
39
|
+
return Path(__file__).parent / "bin"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_bin_path():
|
|
43
|
+
name = f"{BINARY}.exe" if platform.system() == "Windows" else BINARY
|
|
44
|
+
return get_bin_dir() / name
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _fetch(url):
|
|
48
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ifchange-pypi"})
|
|
49
|
+
with urllib.request.urlopen(req) as resp:
|
|
50
|
+
return resp.read()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _verify_checksum(data, archive_name, checksums_data):
|
|
54
|
+
for line in checksums_data.decode().splitlines():
|
|
55
|
+
if archive_name in line:
|
|
56
|
+
expected = line.strip().split()[0]
|
|
57
|
+
actual = hashlib.sha256(data).hexdigest()
|
|
58
|
+
if expected != actual:
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
f"Checksum mismatch: expected {expected}, got {actual}"
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
raise RuntimeError(f"Checksum not found for {archive_name}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def download_binary():
|
|
67
|
+
bin_path = get_bin_path()
|
|
68
|
+
if bin_path.exists():
|
|
69
|
+
return bin_path
|
|
70
|
+
|
|
71
|
+
target = get_target()
|
|
72
|
+
version = f"v{__version__}"
|
|
73
|
+
is_windows = platform.system() == "Windows"
|
|
74
|
+
ext = "zip" if is_windows else "tar.gz"
|
|
75
|
+
archive_name = f"{BINARY}-{version}-{target}.{ext}"
|
|
76
|
+
|
|
77
|
+
base_url = f"https://github.com/{REPO}/releases/download/{version}"
|
|
78
|
+
archive_url = f"{base_url}/{archive_name}"
|
|
79
|
+
checksums_url = f"{base_url}/SHA256SUMS"
|
|
80
|
+
|
|
81
|
+
print(f"Downloading {archive_name}...", file=sys.stderr)
|
|
82
|
+
archive_data = _fetch(archive_url)
|
|
83
|
+
checksums_data = _fetch(checksums_url)
|
|
84
|
+
|
|
85
|
+
_verify_checksum(archive_data, archive_name, checksums_data)
|
|
86
|
+
print("Checksum verified.", file=sys.stderr)
|
|
87
|
+
|
|
88
|
+
# Extract binary
|
|
89
|
+
bin_name = f"{BINARY}.exe" if is_windows else BINARY
|
|
90
|
+
extracted = None
|
|
91
|
+
|
|
92
|
+
if ext == "zip":
|
|
93
|
+
with zipfile.ZipFile(io.BytesIO(archive_data)) as zf:
|
|
94
|
+
for name in zf.namelist():
|
|
95
|
+
if name.endswith(bin_name):
|
|
96
|
+
extracted = zf.read(name)
|
|
97
|
+
break
|
|
98
|
+
else:
|
|
99
|
+
with tarfile.open(fileobj=io.BytesIO(archive_data), mode="r:gz") as tf:
|
|
100
|
+
for member in tf.getmembers():
|
|
101
|
+
# Guard against path traversal (e.g. ../../etc/passwd)
|
|
102
|
+
if member.name.startswith("/") or ".." in member.name.split("/"):
|
|
103
|
+
raise RuntimeError(f"Unsafe path in archive: {member.name}")
|
|
104
|
+
if member.name.endswith(bin_name) and member.isfile():
|
|
105
|
+
f = tf.extractfile(member)
|
|
106
|
+
if f:
|
|
107
|
+
extracted = f.read()
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
if not extracted:
|
|
111
|
+
raise RuntimeError(f"Could not find {bin_name} in archive")
|
|
112
|
+
|
|
113
|
+
bin_dir = get_bin_dir()
|
|
114
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
bin_path.write_bytes(extracted)
|
|
116
|
+
bin_path.chmod(bin_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
117
|
+
|
|
118
|
+
print(f"Installed {BINARY} to {bin_path}", file=sys.stderr)
|
|
119
|
+
return bin_path
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ifchange"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A fast linter for enforcing conditional change directives in source code"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/slnc/ifchange"
|
|
22
|
+
Repository = "https://github.com/slnc/ifchange"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
ifchange = "ifchange.__main__:main"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["ifchange"]
|