pkglint 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.
- pkglint-0.1.0/.gitignore +5 -0
- pkglint-0.1.0/LICENSE +21 -0
- pkglint-0.1.0/PKG-INFO +123 -0
- pkglint-0.1.0/README.md +87 -0
- pkglint-0.1.0/build-constraints.txt +58 -0
- pkglint-0.1.0/go.mod +5 -0
- pkglint-0.1.0/go.sum +12 -0
- pkglint-0.1.0/hatch_build.py +67 -0
- pkglint-0.1.0/internal/pkgbuild/pkgbuild.go +310 -0
- pkglint-0.1.0/internal/pkgbuild/source.go +129 -0
- pkglint-0.1.0/internal/pkgbuild/srcinfo.go +42 -0
- pkglint-0.1.0/internal/report/report.go +105 -0
- pkglint-0.1.0/internal/report/report_test.go +68 -0
- pkglint-0.1.0/internal/rules/consistency.go +93 -0
- pkglint-0.1.0/internal/rules/exec.go +273 -0
- pkglint-0.1.0/internal/rules/fs.go +169 -0
- pkglint-0.1.0/internal/rules/hermetic.go +299 -0
- pkglint-0.1.0/internal/rules/integrity.go +297 -0
- pkglint-0.1.0/internal/rules/rules.go +298 -0
- pkglint-0.1.0/internal/rules/rules_test.go +497 -0
- pkglint-0.1.0/internal/rules/scriptlet.go +114 -0
- pkglint-0.1.0/internal/rules/words.go +142 -0
- pkglint-0.1.0/main.go +99 -0
- pkglint-0.1.0/main_test.go +63 -0
- pkglint-0.1.0/pyproject.toml +47 -0
- pkglint-0.1.0/site/main.go +396 -0
- pkglint-0.1.0/site/render.go +73 -0
- pkglint-0.1.0/site/templates/base.html +58 -0
- pkglint-0.1.0/site/templates/index.html +55 -0
- pkglint-0.1.0/site/templates/package.html +45 -0
- pkglint-0.1.0/site/templates/rule.html +19 -0
pkglint-0.1.0/.gitignore
ADDED
pkglint-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jamison Lahman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pkglint-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pkglint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Security-focused linter for Arch Linux PKGBUILDs
|
|
5
|
+
Project-URL: Repository, https://github.com/jmelahman/pkglint
|
|
6
|
+
Author-email: Jamison Lahman <jamison@lahman.dev>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Jamison Lahman
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Keywords: archlinux,aur,linter,pkgbuild,security,supply-chain,tooling,tools
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: MacOS
|
|
32
|
+
Classifier: Operating System :: POSIX
|
|
33
|
+
Classifier: Programming Language :: Go
|
|
34
|
+
Requires-Python: >=3.6
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# pkglint
|
|
38
|
+
|
|
39
|
+
A security-focused linter for Arch Linux PKGBUILDs.
|
|
40
|
+
|
|
41
|
+
pkglint statically analyzes PKGBUILDs and their install scriptlets — **without ever
|
|
42
|
+
sourcing them** — and reports findings about source integrity, build hermeticity, code
|
|
43
|
+
execution, and persistence patterns, condensed into a letter grade per package. It is
|
|
44
|
+
built on a real bash AST ([mvdan.cc/sh](https://github.com/mvdan/sh)), so the
|
|
45
|
+
quoting/line-continuation tricks that evade regex-based scanners don't work here.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
$ pkglint ~/pkgbuilds/somepkg
|
|
49
|
+
somepkg: grade F, 3 finding(s)
|
|
50
|
+
PKGBUILD:16:3: critical [PB304] a network download is piped straight into bash and executed
|
|
51
|
+
PKGBUILD:11:1: error [PB101] remote source "http://..." has no checksum (SKIP): the download is never verified
|
|
52
|
+
PKGBUILD:24:3: error [PB402] sudo escalates privileges during a build; ...
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```shell
|
|
58
|
+
go install github.com/jmelahman/pkglint@latest
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```shell
|
|
64
|
+
pkglint [flags] [path ...] # paths are package dirs or PKGBUILD files (default: .)
|
|
65
|
+
|
|
66
|
+
--format text|json # output format
|
|
67
|
+
--fail-on SEVERITY # exit 1 at or above: info, warn, error (default), critical, never
|
|
68
|
+
--ignore PB105,PB206 # disable rules
|
|
69
|
+
--rules # list every rule with its documentation
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Suppress a reviewed, intentional finding inline:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# pkglint: ignore=PB204
|
|
76
|
+
go build -o "$pkgname" .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Rules
|
|
80
|
+
|
|
81
|
+
| Group | Rules | What they catch |
|
|
82
|
+
|-------|-------|-----------------|
|
|
83
|
+
| Integrity | PB101–PB107 | SKIP/weak checksums, unpinned VCS sources, unencrypted transports, source/url domain mismatches, DLAGENTS overrides |
|
|
84
|
+
| Hermeticity | PB201–PB206 | network access outside `prepare()`, `pip` without `--require-hashes`, unlocked `cargo`, implicit Go module downloads, disabled checksum databases |
|
|
85
|
+
| Execution | PB301–PB307 | top-level code, `eval`, decode-and-execute, download-and-execute (including `eval "$(curl ...)"` and `source <(wget ...)"` variants), `/dev/tcp`, unresolvable command names, embedded payloads |
|
|
86
|
+
| Filesystem | PB401–PB403 | writes outside `$srcdir`/`$pkgdir`, privilege escalation, setuid files |
|
|
87
|
+
| Scriptlets | PB501–PB502 | network access and persistence (crontabs, systemd units, shell profiles, login-capable users) in `.install` files running as root |
|
|
88
|
+
| Consistency | PB601–PB602 | PKGBUILD / .SRCINFO drift, network access in `pkgver()` |
|
|
89
|
+
|
|
90
|
+
`pkglint --rules` prints the full documentation for each.
|
|
91
|
+
|
|
92
|
+
Grading: any critical → **F**, any error → **D**, 3+ warns → **C**, 1–2 warns → **B**,
|
|
93
|
+
otherwise **A**.
|
|
94
|
+
|
|
95
|
+
A grade is a **static hygiene score, not a malware verdict** — it measures how reviewable
|
|
96
|
+
and reproducible a PKGBUILD is. A low grade means "worth reviewing", never "malicious",
|
|
97
|
+
and a high grade is not an endorsement. Static analysis cannot catch a malicious upstream
|
|
98
|
+
release pinned with a perfectly valid checksum.
|
|
99
|
+
|
|
100
|
+
## Report card site
|
|
101
|
+
|
|
102
|
+
`site/` generates a static "AUR Report Card" — grades, per-package finding pages,
|
|
103
|
+
per-rule documentation pages, `results.json`, and embeddable SVG badges:
|
|
104
|
+
|
|
105
|
+
```shell
|
|
106
|
+
go run ./site -maintainer Jamison -top 500 -out public
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
It downloads the AUR metadata dump once a day, fetches package snapshots politely
|
|
110
|
+
(throttled, cached by `LastModified`), and scans everything in-process.
|
|
111
|
+
|
|
112
|
+
## Roadmap
|
|
113
|
+
|
|
114
|
+
- A `makepkg` shim so AUR helpers lint before building (`yay --makepkg pkglint-makepkg`,
|
|
115
|
+
paru `[bin] Makepkg`)
|
|
116
|
+
- Sandboxed builds: containerized `makepkg` with the package artifact installed on the
|
|
117
|
+
host via `pacman -U`
|
|
118
|
+
- Hermetic builds: two-phase `makepkg -o` (network) / `makepkg -e` (`--network=none`),
|
|
119
|
+
with these lint rules enforcing the conventions that make that split work
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
pkglint-0.1.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# pkglint
|
|
2
|
+
|
|
3
|
+
A security-focused linter for Arch Linux PKGBUILDs.
|
|
4
|
+
|
|
5
|
+
pkglint statically analyzes PKGBUILDs and their install scriptlets — **without ever
|
|
6
|
+
sourcing them** — and reports findings about source integrity, build hermeticity, code
|
|
7
|
+
execution, and persistence patterns, condensed into a letter grade per package. It is
|
|
8
|
+
built on a real bash AST ([mvdan.cc/sh](https://github.com/mvdan/sh)), so the
|
|
9
|
+
quoting/line-continuation tricks that evade regex-based scanners don't work here.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
$ pkglint ~/pkgbuilds/somepkg
|
|
13
|
+
somepkg: grade F, 3 finding(s)
|
|
14
|
+
PKGBUILD:16:3: critical [PB304] a network download is piped straight into bash and executed
|
|
15
|
+
PKGBUILD:11:1: error [PB101] remote source "http://..." has no checksum (SKIP): the download is never verified
|
|
16
|
+
PKGBUILD:24:3: error [PB402] sudo escalates privileges during a build; ...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
go install github.com/jmelahman/pkglint@latest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
pkglint [flags] [path ...] # paths are package dirs or PKGBUILD files (default: .)
|
|
29
|
+
|
|
30
|
+
--format text|json # output format
|
|
31
|
+
--fail-on SEVERITY # exit 1 at or above: info, warn, error (default), critical, never
|
|
32
|
+
--ignore PB105,PB206 # disable rules
|
|
33
|
+
--rules # list every rule with its documentation
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Suppress a reviewed, intentional finding inline:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# pkglint: ignore=PB204
|
|
40
|
+
go build -o "$pkgname" .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Rules
|
|
44
|
+
|
|
45
|
+
| Group | Rules | What they catch |
|
|
46
|
+
|-------|-------|-----------------|
|
|
47
|
+
| Integrity | PB101–PB107 | SKIP/weak checksums, unpinned VCS sources, unencrypted transports, source/url domain mismatches, DLAGENTS overrides |
|
|
48
|
+
| Hermeticity | PB201–PB206 | network access outside `prepare()`, `pip` without `--require-hashes`, unlocked `cargo`, implicit Go module downloads, disabled checksum databases |
|
|
49
|
+
| Execution | PB301–PB307 | top-level code, `eval`, decode-and-execute, download-and-execute (including `eval "$(curl ...)"` and `source <(wget ...)"` variants), `/dev/tcp`, unresolvable command names, embedded payloads |
|
|
50
|
+
| Filesystem | PB401–PB403 | writes outside `$srcdir`/`$pkgdir`, privilege escalation, setuid files |
|
|
51
|
+
| Scriptlets | PB501–PB502 | network access and persistence (crontabs, systemd units, shell profiles, login-capable users) in `.install` files running as root |
|
|
52
|
+
| Consistency | PB601–PB602 | PKGBUILD / .SRCINFO drift, network access in `pkgver()` |
|
|
53
|
+
|
|
54
|
+
`pkglint --rules` prints the full documentation for each.
|
|
55
|
+
|
|
56
|
+
Grading: any critical → **F**, any error → **D**, 3+ warns → **C**, 1–2 warns → **B**,
|
|
57
|
+
otherwise **A**.
|
|
58
|
+
|
|
59
|
+
A grade is a **static hygiene score, not a malware verdict** — it measures how reviewable
|
|
60
|
+
and reproducible a PKGBUILD is. A low grade means "worth reviewing", never "malicious",
|
|
61
|
+
and a high grade is not an endorsement. Static analysis cannot catch a malicious upstream
|
|
62
|
+
release pinned with a perfectly valid checksum.
|
|
63
|
+
|
|
64
|
+
## Report card site
|
|
65
|
+
|
|
66
|
+
`site/` generates a static "AUR Report Card" — grades, per-package finding pages,
|
|
67
|
+
per-rule documentation pages, `results.json`, and embeddable SVG badges:
|
|
68
|
+
|
|
69
|
+
```shell
|
|
70
|
+
go run ./site -maintainer Jamison -top 500 -out public
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
It downloads the AUR metadata dump once a day, fetches package snapshots politely
|
|
74
|
+
(throttled, cached by `LastModified`), and scans everything in-process.
|
|
75
|
+
|
|
76
|
+
## Roadmap
|
|
77
|
+
|
|
78
|
+
- A `makepkg` shim so AUR helpers lint before building (`yay --makepkg pkglint-makepkg`,
|
|
79
|
+
paru `[bin] Makepkg`)
|
|
80
|
+
- Sandboxed builds: containerized `makepkg` with the package artifact installed on the
|
|
81
|
+
host via `pacman -U`
|
|
82
|
+
- Hermetic builds: two-phase `makepkg -o` (network) / `makepkg -e` (`--network=none`),
|
|
83
|
+
with these lint rules enforcing the conventions that make that split work
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# This file was autogenerated by uv via the following command:
|
|
2
|
+
# uv pip compile - -o build-constraints.txt --universal --generate-hashes
|
|
3
|
+
go-bin==1.26.6 \
|
|
4
|
+
--hash=sha256:13a3a133530ffd2005b2ce805a93b8bf3f4712807af16f31aeea6aecfaa55721 \
|
|
5
|
+
--hash=sha256:1f4b3a6f8d7987c032d821ebb69e84ab9be7913b1cd8c45514d9aea21e3dfb7d \
|
|
6
|
+
--hash=sha256:2440ec27b056a19e9ce7c7e4c323f93a1ec0aa220b83838b9adf319487091582 \
|
|
7
|
+
--hash=sha256:2f8d76337bdf2e8e12b655c2f2c4827a202d4198c5cb87f0fde8b286bfa5b816 \
|
|
8
|
+
--hash=sha256:46658f18ce37d71122d25fa48bd7457cf46a3389345e86c5b6decabf1d5ec6ad \
|
|
9
|
+
--hash=sha256:565537475730612936bf42edddff5d627133c108d9a01561f8033ed09dc0c2ff \
|
|
10
|
+
--hash=sha256:7f30d0141d8052d9349b6e26c679ff51d14465ae3362ca9d1623ba1076e376a4 \
|
|
11
|
+
--hash=sha256:ab703942cbb119637e5306085b6f1f14eb51bd6a98b56dd2ff0a06b5afb742f8 \
|
|
12
|
+
--hash=sha256:c0dbc08e9a5a827966c8f2d6fb627668e78ab0f9732035564e24d046a185d92b \
|
|
13
|
+
--hash=sha256:efc17389b78138c27ce3c74275a146ce5e2b39c3969f57a170bbcfe303baf91b
|
|
14
|
+
hatch-vcs==0.5.0 \
|
|
15
|
+
--hash=sha256:0395fa126940340215090c344a2bf4e2a77bcbe7daab16f41b37b98c95809ff9 \
|
|
16
|
+
--hash=sha256:b49677dbdc597460cc22d01b27ab3696f5e16a21ecf2700fb01bc28e1f2a04a7
|
|
17
|
+
hatchling==1.32.0 \
|
|
18
|
+
--hash=sha256:0bdbde4a52b06c37e3eca395f85a762bf0ef06fe374fd8ae429dc6be10230f5f \
|
|
19
|
+
--hash=sha256:0e17c9c3b9aa7c625acc8d0f5b622f107d5049af9ecf5ada4de1aada5be7cdbc
|
|
20
|
+
# via hatch-vcs
|
|
21
|
+
manygo==0.2.0 \
|
|
22
|
+
--hash=sha256:322567f555f0c9896e163f4cecdcd5aa6402996933a6a8ec1cea7f67dae9accd \
|
|
23
|
+
--hash=sha256:90a951179f0d294d7063a4fc5446b52646cff140c386f9c92ca391116a287098
|
|
24
|
+
packaging==26.3 \
|
|
25
|
+
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
|
26
|
+
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
|
27
|
+
# via
|
|
28
|
+
# hatchling
|
|
29
|
+
# setuptools-scm
|
|
30
|
+
# vcs-versioning
|
|
31
|
+
pathspec==1.1.1 \
|
|
32
|
+
--hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
|
|
33
|
+
--hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
|
|
34
|
+
# via hatchling
|
|
35
|
+
pluggy==1.6.0 \
|
|
36
|
+
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
|
37
|
+
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
|
38
|
+
# via hatchling
|
|
39
|
+
setuptools==84.0.0 \
|
|
40
|
+
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
|
|
41
|
+
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
|
|
42
|
+
# via setuptools-scm
|
|
43
|
+
setuptools-scm==10.2.1 \
|
|
44
|
+
--hash=sha256:4fa7dd82cf8c800df59c9a288c90299b1657ff1ecfc3f5cc00287c5dbf5e27a9 \
|
|
45
|
+
--hash=sha256:b7c82f4102d389ee57dc66ccdb4f9b4bca3c40ba83b43f1f63d68ccd72db2580
|
|
46
|
+
# via hatch-vcs
|
|
47
|
+
tomlkit==0.15.1 \
|
|
48
|
+
--hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \
|
|
49
|
+
--hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97
|
|
50
|
+
# via hatchling
|
|
51
|
+
trove-classifiers==2026.6.1.19 \
|
|
52
|
+
--hash=sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 \
|
|
53
|
+
--hash=sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745
|
|
54
|
+
# via hatchling
|
|
55
|
+
vcs-versioning==2.3.1 \
|
|
56
|
+
--hash=sha256:806635bd0ea653c96af98a70624be758d408a989f2cd0b390e63474d99f96b63 \
|
|
57
|
+
--hash=sha256:a11299394e101569a62ab061375260d08bc56cd33d685b7067d85312368f4457
|
|
58
|
+
# via setuptools-scm
|
pkglint-0.1.0/go.mod
ADDED
pkglint-0.1.0/go.sum
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
|
2
|
+
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
|
3
|
+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
4
|
+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
5
|
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
6
|
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
7
|
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
8
|
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
9
|
+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
|
10
|
+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
|
11
|
+
mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk=
|
|
12
|
+
mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
9
|
+
|
|
10
|
+
import manygo
|
|
11
|
+
|
|
12
|
+
# Central pin for the Go toolchain, so builds are as hermetic as PEP 517
|
|
13
|
+
# allows: the same compiler version everywhere, pinned in exactly one place.
|
|
14
|
+
# The static backend pins (hatchling, hatch-vcs, manygo) live in
|
|
15
|
+
# pyproject.toml [build-system].requires.
|
|
16
|
+
GO_BIN_PIN = "go-bin==1.26.6"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GoBinaryBuildHook(BuildHookInterface):
|
|
20
|
+
def dependencies(self) -> list[str]:
|
|
21
|
+
"""Wheel builds always use the pinned Go toolchain.
|
|
22
|
+
|
|
23
|
+
Never probing PATH keeps builds hermetic: the binary is produced by
|
|
24
|
+
the same compiler version on every machine. The sdist carries only
|
|
25
|
+
sources, so it doesn't need one.
|
|
26
|
+
"""
|
|
27
|
+
if self.target_name != "wheel":
|
|
28
|
+
return []
|
|
29
|
+
return [GO_BIN_PIN]
|
|
30
|
+
|
|
31
|
+
def initialize(self, version, build_data) -> None: # noqa: ANN001, ARG002
|
|
32
|
+
if self.target_name != "wheel":
|
|
33
|
+
# The sdist carries sources only; the binary is built when the
|
|
34
|
+
# sdist is turned into a wheel on the installing machine.
|
|
35
|
+
return
|
|
36
|
+
build_data["pure_python"] = False
|
|
37
|
+
goos = os.getenv("GOOS")
|
|
38
|
+
goarch = os.getenv("GOARCH")
|
|
39
|
+
if manygo.is_goos(goos) and manygo.is_goarch(goarch):
|
|
40
|
+
build_data["tag"] = "py3-none-" + manygo.get_platform_tag(goos=goos, goarch=goarch)
|
|
41
|
+
else:
|
|
42
|
+
# Native build: let hatchling tag the wheel for this platform.
|
|
43
|
+
build_data["infer_tag"] = True
|
|
44
|
+
binary_name = self.config["binary_name"]
|
|
45
|
+
|
|
46
|
+
go = shutil.which("go")
|
|
47
|
+
if go is None:
|
|
48
|
+
raise RuntimeError("go is required to build the pkglint binary")
|
|
49
|
+
env = os.environ.copy()
|
|
50
|
+
# Pure Go: static binaries on every target, no C toolchain needed.
|
|
51
|
+
env["CGO_ENABLED"] = "0"
|
|
52
|
+
|
|
53
|
+
# Always rebuild: a leftover binary from another target would be
|
|
54
|
+
# packaged into a wheel tagged for the wrong platform.
|
|
55
|
+
print(f"Building Go binary '{binary_name}'...")
|
|
56
|
+
subprocess.check_call( # noqa: S603
|
|
57
|
+
[
|
|
58
|
+
go, "build", "-trimpath",
|
|
59
|
+
"-ldflags", f"-s -w -X main.version={self.metadata.version}",
|
|
60
|
+
"-o", str(Path(self.root) / binary_name),
|
|
61
|
+
".",
|
|
62
|
+
],
|
|
63
|
+
cwd=self.root,
|
|
64
|
+
env=env,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
build_data["shared_scripts"] = {binary_name: binary_name}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// Package pkgbuild statically parses PKGBUILD and .install files.
|
|
2
|
+
//
|
|
3
|
+
// PKGBUILDs are untrusted input: they must never be sourced or executed
|
|
4
|
+
// (even `makepkg --printsrcinfo` runs top-level code). Everything here is
|
|
5
|
+
// pure static analysis on the bash AST via mvdan.cc/sh.
|
|
6
|
+
package pkgbuild
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"bytes"
|
|
10
|
+
"fmt"
|
|
11
|
+
"os"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"regexp"
|
|
14
|
+
"strings"
|
|
15
|
+
|
|
16
|
+
"mvdan.cc/sh/v3/syntax"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// Var is a top-level variable assignment in a PKGBUILD.
|
|
20
|
+
type Var struct {
|
|
21
|
+
Name string
|
|
22
|
+
Values []string // rendered values; one element for scalar assignments
|
|
23
|
+
Array bool
|
|
24
|
+
Pos syntax.Pos
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Unit is a single bash file under analysis: the PKGBUILD itself or an
|
|
28
|
+
// install scriptlet.
|
|
29
|
+
type Unit struct {
|
|
30
|
+
Path string
|
|
31
|
+
Raw []byte
|
|
32
|
+
File *syntax.File
|
|
33
|
+
Scriptlet bool
|
|
34
|
+
Functions map[string]*syntax.FuncDecl
|
|
35
|
+
TopLevel []*syntax.Stmt // top-level statements that are not function declarations
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Package is a fully loaded package directory.
|
|
39
|
+
type Package struct {
|
|
40
|
+
Dir string
|
|
41
|
+
PKGBUILD Unit
|
|
42
|
+
Scriptlets []Unit
|
|
43
|
+
Vars map[string]*Var
|
|
44
|
+
SrcInfo *SrcInfo // nil when no .SRCINFO is present
|
|
45
|
+
|
|
46
|
+
// Suppressions maps line number -> rule IDs disabled on that line via
|
|
47
|
+
// "# pkglint: ignore=PB123[,PB456]" (applies to the same and next line).
|
|
48
|
+
Suppressions map[int]map[string]bool
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// newParser returns a fresh parser per parse: syntax.Parser is not safe for
|
|
52
|
+
// concurrent use, and Load is called from concurrent scanners.
|
|
53
|
+
func newParser() *syntax.Parser {
|
|
54
|
+
return syntax.NewParser(syntax.KeepComments(true), syntax.Variant(syntax.LangBash))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Load reads the PKGBUILD at path (a file or a directory containing one),
|
|
58
|
+
// plus any install scriptlets referenced by install= or living next to it.
|
|
59
|
+
func Load(path string) (*Package, error) {
|
|
60
|
+
info, err := os.Stat(path)
|
|
61
|
+
if err != nil {
|
|
62
|
+
return nil, err
|
|
63
|
+
}
|
|
64
|
+
dir, file := filepath.Split(path)
|
|
65
|
+
if info.IsDir() {
|
|
66
|
+
dir = path
|
|
67
|
+
file = "PKGBUILD"
|
|
68
|
+
}
|
|
69
|
+
pkgPath := filepath.Join(dir, file)
|
|
70
|
+
|
|
71
|
+
raw, err := os.ReadFile(pkgPath)
|
|
72
|
+
if err != nil {
|
|
73
|
+
return nil, err
|
|
74
|
+
}
|
|
75
|
+
unit, err := parseUnit(pkgPath, raw, false)
|
|
76
|
+
if err != nil {
|
|
77
|
+
return nil, err
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pkg := &Package{
|
|
81
|
+
Dir: dir,
|
|
82
|
+
PKGBUILD: unit,
|
|
83
|
+
Vars: map[string]*Var{},
|
|
84
|
+
Suppressions: parseSuppressions(raw),
|
|
85
|
+
}
|
|
86
|
+
pkg.extractTopLevel()
|
|
87
|
+
|
|
88
|
+
if data, err := os.ReadFile(filepath.Join(dir, ".SRCINFO")); err == nil {
|
|
89
|
+
pkg.SrcInfo = ParseSrcInfo(data)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for _, name := range pkg.installFiles() {
|
|
93
|
+
p := filepath.Join(dir, name)
|
|
94
|
+
data, err := os.ReadFile(p)
|
|
95
|
+
if err != nil {
|
|
96
|
+
continue // missing scriptlet is reported by a rule
|
|
97
|
+
}
|
|
98
|
+
su, err := parseUnit(p, data, true)
|
|
99
|
+
if err != nil {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
pkg.Scriptlets = append(pkg.Scriptlets, su)
|
|
103
|
+
for line, ids := range parseSuppressions(data) {
|
|
104
|
+
pkg.Suppressions[line] = ids // best effort; line collisions across files are acceptable
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return pkg, nil
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func parseUnit(path string, raw []byte, scriptlet bool) (Unit, error) {
|
|
111
|
+
f, err := newParser().Parse(bytes.NewReader(raw), path)
|
|
112
|
+
if err != nil {
|
|
113
|
+
return Unit{}, fmt.Errorf("parse %s: %w", path, err)
|
|
114
|
+
}
|
|
115
|
+
u := Unit{
|
|
116
|
+
Path: path,
|
|
117
|
+
Raw: raw,
|
|
118
|
+
File: f,
|
|
119
|
+
Scriptlet: scriptlet,
|
|
120
|
+
Functions: map[string]*syntax.FuncDecl{},
|
|
121
|
+
}
|
|
122
|
+
for _, stmt := range f.Stmts {
|
|
123
|
+
if fd, ok := stmt.Cmd.(*syntax.FuncDecl); ok {
|
|
124
|
+
u.Functions[fd.Name.Value] = fd
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
u.TopLevel = append(u.TopLevel, stmt)
|
|
128
|
+
}
|
|
129
|
+
return u, nil
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// extractTopLevel records top-level variable assignments from the PKGBUILD.
|
|
133
|
+
func (p *Package) extractTopLevel() {
|
|
134
|
+
record := func(as *syntax.Assign) {
|
|
135
|
+
if as.Name == nil {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
v := &Var{Name: as.Name.Value, Pos: as.Pos()}
|
|
139
|
+
if as.Array != nil {
|
|
140
|
+
v.Array = true
|
|
141
|
+
for _, el := range as.Array.Elems {
|
|
142
|
+
s, _ := RenderWord(el.Value, nil)
|
|
143
|
+
v.Values = append(v.Values, s)
|
|
144
|
+
}
|
|
145
|
+
} else if as.Value != nil {
|
|
146
|
+
s, _ := RenderWord(as.Value, nil)
|
|
147
|
+
v.Values = []string{s}
|
|
148
|
+
}
|
|
149
|
+
p.Vars[v.Name] = v
|
|
150
|
+
}
|
|
151
|
+
for _, stmt := range p.PKGBUILD.TopLevel {
|
|
152
|
+
switch cmd := stmt.Cmd.(type) {
|
|
153
|
+
case *syntax.CallExpr:
|
|
154
|
+
if len(cmd.Args) == 0 {
|
|
155
|
+
for _, as := range cmd.Assigns {
|
|
156
|
+
record(as)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
case *syntax.DeclClause:
|
|
160
|
+
for _, as := range cmd.Args {
|
|
161
|
+
record(as)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Scalar returns the rendered value of a scalar variable, expanded against
|
|
168
|
+
// other known scalars.
|
|
169
|
+
func (p *Package) Scalar(name string) (string, bool) {
|
|
170
|
+
v, ok := p.Vars[name]
|
|
171
|
+
if !ok || v.Array || len(v.Values) != 1 {
|
|
172
|
+
return "", false
|
|
173
|
+
}
|
|
174
|
+
return p.Expand(v.Values[0]), true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
var varRef = regexp.MustCompile(`\$(\{[A-Za-z_][A-Za-z0-9_]*\}|[A-Za-z_][A-Za-z0-9_]*)`)
|
|
178
|
+
|
|
179
|
+
// Expand substitutes $name / ${name} references using known top-level scalar
|
|
180
|
+
// variables. Unknown references are left as-is.
|
|
181
|
+
func (p *Package) Expand(s string) string {
|
|
182
|
+
for range 5 {
|
|
183
|
+
if !strings.Contains(s, "$") {
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
out := varRef.ReplaceAllStringFunc(s, func(m string) string {
|
|
187
|
+
name := strings.Trim(m[1:], "{}")
|
|
188
|
+
if v, ok := p.Vars[name]; ok && !v.Array && len(v.Values) == 1 {
|
|
189
|
+
return v.Values[0]
|
|
190
|
+
}
|
|
191
|
+
return m
|
|
192
|
+
})
|
|
193
|
+
if out == s {
|
|
194
|
+
break
|
|
195
|
+
}
|
|
196
|
+
s = out
|
|
197
|
+
}
|
|
198
|
+
return s
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// installFiles returns the scriptlet filenames referenced by install= or
|
|
202
|
+
// declared per split package, deduplicated.
|
|
203
|
+
func (p *Package) installFiles() []string {
|
|
204
|
+
seen := map[string]bool{}
|
|
205
|
+
var out []string
|
|
206
|
+
add := func(name string) {
|
|
207
|
+
name = strings.TrimSpace(name)
|
|
208
|
+
if name == "" || seen[name] {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
seen[name] = true
|
|
212
|
+
out = append(out, name)
|
|
213
|
+
}
|
|
214
|
+
if v, ok := p.Vars["install"]; ok {
|
|
215
|
+
for _, val := range v.Values {
|
|
216
|
+
add(p.Expand(val))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if p.SrcInfo != nil {
|
|
220
|
+
for _, val := range p.SrcInfo.All("install") {
|
|
221
|
+
add(val)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return out
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Units returns the PKGBUILD followed by any parsed scriptlets.
|
|
228
|
+
func (p *Package) Units() []Unit {
|
|
229
|
+
return append([]Unit{p.PKGBUILD}, p.Scriptlets...)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
var suppressRe = regexp.MustCompile(`#\s*pkglint:\s*ignore=([A-Z0-9, ]+)`)
|
|
233
|
+
|
|
234
|
+
func parseSuppressions(raw []byte) map[int]map[string]bool {
|
|
235
|
+
out := map[int]map[string]bool{}
|
|
236
|
+
for i, line := range strings.Split(string(raw), "\n") {
|
|
237
|
+
m := suppressRe.FindStringSubmatch(line)
|
|
238
|
+
if m == nil {
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
ids := map[string]bool{}
|
|
242
|
+
for _, id := range strings.Split(m[1], ",") {
|
|
243
|
+
if id = strings.TrimSpace(id); id != "" {
|
|
244
|
+
ids[id] = true
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
out[i+1] = ids
|
|
248
|
+
}
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Suppressed reports whether ruleID is suppressed at the given line by a
|
|
253
|
+
// directive on that line or the line above it.
|
|
254
|
+
func (p *Package) Suppressed(ruleID string, line int) bool {
|
|
255
|
+
for _, l := range []int{line, line - 1} {
|
|
256
|
+
if ids, ok := p.Suppressions[l]; ok && ids[ruleID] {
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// RenderWord returns an approximate textual form of a word. dynamic is true
|
|
264
|
+
// when the word contains constructs whose value cannot be determined
|
|
265
|
+
// statically (command substitution, process substitution, arithmetic,
|
|
266
|
+
// indirection, or parameter operations). Plain references to unknown
|
|
267
|
+
// variables render as "$name" and are not considered dynamic, so prefix
|
|
268
|
+
// checks like `$pkgdir/...` still work. vars, when non-nil, resolves simple
|
|
269
|
+
// parameter references.
|
|
270
|
+
func RenderWord(w *syntax.Word, vars map[string]string) (s string, dynamic bool) {
|
|
271
|
+
if w == nil {
|
|
272
|
+
return "", false
|
|
273
|
+
}
|
|
274
|
+
var b strings.Builder
|
|
275
|
+
dyn := renderParts(&b, w.Parts, vars)
|
|
276
|
+
return b.String(), dyn
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
func renderParts(b *strings.Builder, parts []syntax.WordPart, vars map[string]string) bool {
|
|
280
|
+
dynamic := false
|
|
281
|
+
for _, part := range parts {
|
|
282
|
+
switch x := part.(type) {
|
|
283
|
+
case *syntax.Lit:
|
|
284
|
+
b.WriteString(x.Value)
|
|
285
|
+
case *syntax.SglQuoted:
|
|
286
|
+
b.WriteString(x.Value)
|
|
287
|
+
case *syntax.DblQuoted:
|
|
288
|
+
if renderParts(b, x.Parts, vars) {
|
|
289
|
+
dynamic = true
|
|
290
|
+
}
|
|
291
|
+
case *syntax.ParamExp:
|
|
292
|
+
if x.Excl || x.Length || x.Width || x.Index != nil || x.Slice != nil || x.Repl != nil || x.Exp != nil || x.Names != 0 {
|
|
293
|
+
b.WriteString("\x00")
|
|
294
|
+
dynamic = true
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
if vars != nil {
|
|
298
|
+
if v, ok := vars[x.Param.Value]; ok {
|
|
299
|
+
b.WriteString(v)
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
b.WriteString("$" + x.Param.Value)
|
|
304
|
+
default:
|
|
305
|
+
b.WriteString("\x00")
|
|
306
|
+
dynamic = true
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return dynamic
|
|
310
|
+
}
|