Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
560 lines (518 loc) · 29.6 KB
/
Copy pathMakefile
File metadata and controls
560 lines (518 loc) · 29.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
# Shadowfetch Linux — top-level build orchestration
#
# Common targets:
# make deps Install build dependencies (Debian host required)
# make packages Build all custom .deb packages
# make repo Generate the local APT repo from built packages
# make iso Build the bootable ISO (uses sudo; needs local repo)
# make qemu Boot the built ISO in QEMU for testing
# make clean Clean the live-build tree
# make distclean Wipe everything regenerable
SHELL := /bin/bash
VERSION ?= 5.0.1
CODENAME ?= umbra
ISO_NAME := shadowfetch-$(VERSION)-amd64.iso
VERSION_TOKEN := $(subst .,_,$(VERSION))
PUBLIC_SITE ?= https://www.shadowfetchlinux.org
ARTIFACT_BASE ?= https://www.shadowfetch.com/linux
# Real source packages — each has its own debian/ tree.
# shadowfetch-meta builds two metapackages plus the upgrade-compatible,
# driver-free shadowfetch-nvidia setup package.
PACKAGES := \
shadowfetch-meta \
shadowfetch-welcome \
shadowfetch-themes \
shadowfetch-defaults \
shadowfetch-branding \
grub-btrfs \
shadowfetch-ember \
shadowfetch-firewatchd \
shadowfetch-phoenix \
shadowfetch-menus \
shadowfetch-control-center \
shadowfetch-fireproof \
shadowfetch-hwscan \
shadowfetch-fireline \
shadowfetch-missions \
shadowfetch-drkonqi-pickup
# GPG key the APT repo signs with. Override with `make REPO_KEY_ID=...` if
# you've regenerated the key. The default matches the key on shadowfetch-linux.
REPO_KEY_ID ?= 8F13CE1535EE1F4A2916A1F73C5C900B7BE80CA1
# Generate an apt Valid-Until field automatically so clients do not break on
# stale metadata after publication. Reprepro accepts durations such as 14d.
#
# This was 14d, which is only safe for a repository that is republished on a
# schedule. Nothing republishes this one, so every installed machine lost
# `apt update` a fortnight after each release -- the index published with
# 4.0.0 on 2026-09-06 was already down to twelve days when this was found.
# A longer window trades a little rollback-attack protection for a
# repository that stays usable between releases; shorten it again only
# alongside an automated `make refresh-index` job.
REPO_VALID_FOR ?= 180d
# Publication gate minimum remaining Valid-Until lifetime: 24 hours.
REPO_MIN_VALID_FOR_SECONDS ?= 86400
# `sudo make iso` keeps SUDO_USER but changes HOME to /root. Point GnuPG at
# the invoking builder's keyring so repository and ISO signing stay reliable.
BUILDER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
BUILDER_USER := $(if $(SUDO_USER),$(SUDO_USER),$(shell id -un))
BUILDER_GROUP := $(shell id -gn $(BUILDER_USER))
GPG := gpg --homedir $(BUILDER_HOME)/.gnupg
REPREPRO := env GNUPGHOME=$(BUILDER_HOME)/.gnupg reprepro
# Port used to serve the local repo to the chroot during ISO build.
LOCAL_REPO_PORT ?= 8089
ROOT := $(CURDIR)
BUILD_DIR := $(ROOT)/build
REPO_DIR := $(ROOT)/repo
LB_DIR := $(ROOT)/live-build
LB_BUILD_LOG := $(BUILD_DIR)/live-build-$(VERSION).log
LB_BUILD_MARKER := $(BUILD_DIR)/.live-build-$(VERSION)-started
QA_EVIDENCE_DIR := $(ROOT)/work/qa-$(VERSION)/evidence
# RUN-STAMPED, and deliberately not the path ISO-01 records as its evidence.
# This was $(QA_EVIDENCE_DIR)/iso/iso-gate.log -- exactly the file the manifest
# pins -- so `make iso-gate` teed its own verdict into its own evidence, and
# the proof that the ISO gate passed was the gate saying so. It also meant a
# second run overwrote the file whose SHA-256 the manifest had recorded, so
# `acceptance.py verify` then failed on a hash mismatch for a case nobody had
# touched. A gate never writes into the file it will be graded on.
ISO_GATE_RUN := $(shell date -u +%Y%m%dT%H%M%SZ)
ISO_GATE_LOG := $(QA_EVIDENCE_DIR)/iso/runs/iso-gate-$(ISO_GATE_RUN).log
# Stage Q: ONE implementation per gate family plus a version DATA file.
# These used to be $(ROOT)/tools/<family>_$(VERSION_TOKEN).py, so cutting a
# release meant copying four modules and hand-editing the version strings in
# each. Everything that varies per release now lives in RELEASE_DATA, and every
# gate is passed --version so a run can never silently gate the wrong release.
RELEASE_TOOLS := $(ROOT)/tools/release
RELEASE_DATA := $(RELEASE_TOOLS)/versions/$(VERSION).toml
SOURCE_GATE := $(RELEASE_TOOLS)/source_gate.py
PACKAGE_GATE := $(RELEASE_TOOLS)/package_gate.py
ISO_GATE := $(RELEASE_TOOLS)/iso_gate.py
ACCEPTANCE_TOOL := $(RELEASE_TOOLS)/acceptance.py
ACCEPTANCE_MANIFEST := $(ROOT)/qa/$(VERSION)/acceptance.json
# ShadowCode is the one PREBUILT package: its .deb is upstream's signed bytes,
# fetched and authenticated by tools/fetch_shadowcode.py, never built here, so
# it is not in PACKAGES. The version lives only in tools/release/shadowcode.toml;
# this asks the tool for the exact build/ path rather than globbing, so a stale
# previous version can never be picked up.
SHADOWCODE_FETCH := $(ROOT)/tools/fetch_shadowcode.py
SHADOWCODE_DEB := $(shell python3 $(SHADOWCODE_FETCH) --print-build-deb 2>/dev/null)
RELEASE_DEBS := $(BUILD_DIR)/*_$(VERSION)-1_all.deb $(BUILD_DIR)/*_$(VERSION)-1_amd64.deb $(BUILD_DIR)/grub-btrfs_*_all.deb $(SHADOWCODE_DEB)
PACKAGES_STAMP := $(BUILD_DIR)/.packages-$(VERSION)
R2_BUCKET ?= shadowfetch-linux
# Cloudflare R2 S3-compatible endpoint for this account.
# The release publisher uses the S3 API with multipart uploads.
R2_ENDPOINT ?= https://<CLOUDFLARE_ACCOUNT_ID>.r2.cloudflarestorage.com
R2_REGION ?= auto
# Used by sync-from-linux for read-only artifact inspection.
LINUX_HOST ?= shadowfetch-linux
LINUX_PATH ?= ~/projects/shadowfetch-4.0.0
.PHONY: all help test attacks release-data source-gate package-gate iso-gate acceptance-audit acceptance-gate deps packages repo refresh-index check-index iso sign pre-release-check publish qemu clean distclean \
sync-from-linux deploy-worker ship stamp-version shadowcode shadowcode-smoke
all: iso
help:
@echo "Shadowfetch build system"
@echo " make deps Install required build dependencies"
@echo " make test Run focused source and behavior checks"
@echo " make source-gate Run tests, parsers, linters and secret scans"
@echo " make package-gate Validate packages, signed repo and clean install"
@echo " make iso-gate Validate the signed hybrid ISO and installed-image payload"
@echo " make acceptance-audit Report on the release manifest (does NOT fail on pending)"
@echo " make acceptance-gate HARD gate: every required case must pass or be waived"
@echo " make vm-acceptance Run one VM acceptance case (VM_CASE=...) end to end"
@echo " make packages Build all .deb packages"
@echo " make repo Build local APT repository"
@echo " make refresh-index Re-sign the APT indices in place (no rebuild)"
@echo " make check-index Fail if the APT index is expired or expiring"
@echo " make iso Build the bootable ISO (requires sudo)"
@echo " make qemu Boot ISO in QEMU for testing"
@echo " make clean Clean live-build artifacts"
@echo " make distclean Wipe all regenerable files"
test:
bash tools/build_drkonqi_pickup.sh --test
python3 -m unittest discover -s packages/shadowfetch-missions/tests -v
QT_QPA_PLATFORM=offscreen python3 -m unittest discover -s packages/shadowfetch-control-center/tests -v
QT_QPA_PLATFORM=offscreen python3 -m unittest discover -s packages/shadowfetch-welcome/tests -v
python3 -m unittest discover -s packages/shadowfetch-defaults/tests -v
python3 -m unittest discover -s packages/shadowfetch-firewatchd/tests -v
python3 -m unittest discover -s packages/shadowfetch-fireproof/tests -v
python3 -m unittest discover -s packages/shadowfetch-hwscan/tests -v
python3 -m unittest discover -s packages/shadowfetch-phoenix/tests -v
python3 packages/shadowfetch-fireline/tests/test_fireline_mcp.py
python3 packages/shadowfetch-fireline/tests/test_checkpoint_roundtrip.py
python3 packages/shadowfetch-fireline/tests/test_checkpoint_hardening.py
python3 packages/shadowfetch-fireline/tests/test_firebreak_4.py
python3 packages/shadowfetch-fireline/tests/test_fireline_privilege.py
python3 -m unittest discover -s packages/shadowfetch-fireline/tests \
-t packages/shadowfetch-fireline/tests -p 'test_firebreak_records.py' -v
python3 -m unittest discover -s packages/shadowfetch-fireline/tests \
-t packages/shadowfetch-fireline/tests -p 'test_mcp_audit.py' -v
SHADOWFETCH_FIREBREAK_TEST_BIN=$(CURDIR)/packages/shadowfetch-fireline/data/usr/bin/shadowfetch-firebreak \
SHADOWFETCH_CHECKPOINT_BIN=$(CURDIR)/packages/shadowfetch-fireline/data/usr/bin/shadowfetch-checkpoint \
bash packages/shadowfetch-fireline/tests/test_firebreak.sh
python3 -m unittest discover -s tools/tests -v
$(MAKE) attacks
# Adversarial suites. Separate from `test` only in name: `test` runs them.
# They assert what the system REFUSES, which unit tests structurally cannot --
# a test written against the implementation cannot notice a guard the
# implementation never had.
attacks:
python3 tools/attacks/attack_approval.py
python3 tools/attacks/attack_integrity.py
python3 tools/attacks/attack_lifecycle.py
python3 tools/attacks/attack_concurrency.py
python3 tools/attacks/attack_verifier.py
python3 tools/attacks/attack_domain.py
# release-data is a prerequisite of every gate: a missing version file must
# fail here, naming the file to add, rather than inside a gate half a run later.
release-data:
@test -f $(RELEASE_DATA) || { echo "Missing release data: $(RELEASE_DATA) -- cutting a release needs one version file, see tools/release/README.md" >&2; exit 1; }
source-gate: release-data
@test -x $(SOURCE_GATE) || { echo "Missing source gate: $(SOURCE_GATE)" >&2; exit 1; }
$(SOURCE_GATE) --version $(VERSION)
package-gate: repo release-data
@test -x $(PACKAGE_GATE) || { echo "Missing package gate: $(PACKAGE_GATE)" >&2; exit 1; }
$(PACKAGE_GATE) --version $(VERSION)
acceptance-audit: release-data
@test -x $(ACCEPTANCE_TOOL) || { echo "Missing acceptance tool: $(ACCEPTANCE_TOOL)" >&2; exit 1; }
@test -f $(ACCEPTANCE_MANIFEST) || { echo "Missing acceptance manifest: $(ACCEPTANCE_MANIFEST)" >&2; exit 1; }
$(ACCEPTANCE_TOOL) --version $(VERSION) --manifest $(ACCEPTANCE_MANIFEST) acceptance-audit
# The HARD gate. acceptance-audit reports and exits 0 even with pending
# cases, which is how 4.0.0 was published with thirteen of eighteen required
# cases unproven. This target refuses unless every required case is pass or
# waived and every artifact field is recorded. A release must depend on this,
# not on the audit.
acceptance-gate: release-data
@test -x $(ACCEPTANCE_TOOL) || { echo "Missing acceptance tool: $(ACCEPTANCE_TOOL)" >&2; exit 1; }
@test -f $(ACCEPTANCE_MANIFEST) || { echo "Missing acceptance manifest: $(ACCEPTANCE_MANIFEST)" >&2; exit 1; }
$(ACCEPTANCE_TOOL) --version $(VERSION) --manifest $(ACCEPTANCE_MANIFEST) acceptance-gate
# Stage R: automated VM acceptance. The harness boots the artifact under test in
# QEMU/KVM, executes one case against the running machine, captures evidence,
# binds it to the artifact digest and appends a hash-chained receipt -- in ONE
# workflow. There is no TARGET here that marks a case passed: recording is a
# switch on the run, reachable only at the end of a run that happened and only
# for a PASS.
#
# BE PRECISE ABOUT WHAT THAT CLOSES, because this comment used to overstate it.
# It closes the gap for the harness. It does NOT close it for the system:
# `tools/release/acceptance.py record <case> --status pass --evidence <file>`
# is a supported command with no run behind it, and it is how every currently
# recorded pass in the manifest was written. What now stands between that
# command and a false pass is not this target -- it is that `record` stamps the
# artifact digest into each evidence entry and `verify` refuses evidence that
# names a different image or none at all. A person can still type a pass; they
# can no longer type one about nothing.
# Exit status: 0 PASS, 1 FAIL, 2 harness error, 3 BLOCKED. BLOCKED is not a pass.
.PHONY: vm-acceptance vm-acceptance-list vm-acceptance-status vm-acceptance-verify
VM_ACCEPTANCE := $(ROOT)/tools/acceptance/vm_acceptance.py
VM_CASE ?= live-boot
VM_ACCEPTANCE_ARGS ?=
vm-acceptance: release-data
@test -x $(VM_ACCEPTANCE) || { echo "Missing VM acceptance harness: $(VM_ACCEPTANCE)" >&2; exit 1; }
@test -f $(ROOT)/$(ISO_NAME) || { echo "Missing artifact: $(ROOT)/$(ISO_NAME)" >&2; exit 1; }
$(VM_ACCEPTANCE) --version $(VERSION) run --case $(VM_CASE) --artifact $(ROOT)/$(ISO_NAME) $(VM_ACCEPTANCE_ARGS)
vm-acceptance-list:
@$(VM_ACCEPTANCE) list
vm-acceptance-status:
@$(VM_ACCEPTANCE) --version $(VERSION) status
# Re-reads every receipt, every evidence byte and the chain that links the runs.
vm-acceptance-verify:
@$(VM_ACCEPTANCE) --version $(VERSION) verify --all
deps:
sudo apt-get update
sudo apt-get install -y \
live-build live-config live-boot \
debhelper devscripts equivs dh-python lintian \
reprepro gnupg \
desktop-file-utils shellcheck podman \
qemu-system-x86 qemu-utils \
xorriso isolinux syslinux-common syslinux-utils \
grub-pc-bin grub-efi-amd64-bin mtools \
xz-utils python3 python3-yaml curl
packages: $(PACKAGES_STAMP)
# The version lives in exactly one place: VERSION above. It used to also live
# in shadowfetch-branding's data files, hand-edited, and 2.1.0 shipped an image
# whose os-release, version file and Calamares branding all said 2.0.1 -
# hooks/0016 derived the installer string correctly from a source of truth that
# was itself stale. A single source of truth that a human maintains is the same
# bug with one fewer copy, so the build stamps it now.
stamp-version:
python3 tools/stamp_version.py "$(VERSION)" --outstanding-ok
# Fetch the pinned ShadowCode release into build/cache/shadowcode/<version>/,
# authenticate it against the vendored upstream key and the pin, and stage the
# .deb as build/shadow-code_<version>_amd64.deb. Idempotent, and offline once
# the cache holds the assets. See vendor/shadowcode/README.md.
shadowcode:
@test -n "$(SHADOWCODE_DEB)" || { echo "tools/release/shadowcode.toml is unreadable; cannot name the ShadowCode .deb" >&2; exit 1; }
python3 $(SHADOWCODE_FETCH)
# Host-side smoke of the verified .deb: extract, ldd, --version. No VM, no install.
shadowcode-smoke: shadowcode
python3 $(ROOT)/tools/shadowcode_smoke.py
$(PACKAGES_STAMP): stamp-version shadowcode
@mkdir -p $(BUILD_DIR)
@for pkg in $(PACKAGES); do \
echo ">>> Building $$pkg" ; \
if [ "$$pkg" = shadowfetch-drkonqi-pickup ]; then \
bash $(ROOT)/tools/build_drkonqi_pickup.sh --build || exit 1 ; \
else \
( cd $(ROOT)/packages/$$pkg && dpkg-buildpackage -us -uc -b ) || exit 1 ; \
fi ; \
mv $(ROOT)/packages/*.deb $(BUILD_DIR)/ 2>/dev/null || true ; \
mv $(ROOT)/packages/*.changes $(ROOT)/packages/*.buildinfo $(BUILD_DIR)/ 2>/dev/null || true ; \
done
@echo ">>> Built packages:"
@ls -1 $(RELEASE_DEBS)
@touch $@
repo: packages
@rm -rf $(REPO_DIR)/db $(REPO_DIR)/dists $(REPO_DIR)/pool
@mkdir -p $(REPO_DIR)/conf
@printf '%s\n' \
'Origin: Shadowfetch' \
'Label: Shadowfetch' \
'Codename: $(CODENAME)' \
'Architectures: amd64 source' \
'Components: main' \
'Description: Shadowfetch Linux package repository' \
'ValidFor: $(REPO_VALID_FOR)' \
'DebIndices: Packages Release . .gz' \
'DscIndices: Sources Release . .gz' \
'SignWith: $(REPO_KEY_ID)' \
> $(REPO_DIR)/conf/distributions
# Clean any prior pool entries for these packages so reprepro accepts re-includes.
@for deb in $(RELEASE_DEBS); do \
pkg=$$(dpkg-deb -f $$deb Package) ; \
$(REPREPRO) -b $(REPO_DIR) remove $(CODENAME) $$pkg >/dev/null 2>&1 || true ; \
done
# /linux/licensing promises complete corresponding source in main/source.
# Build it from cleaned copies and keep generated artifacts out of the tarballs.
@rm -rf $(BUILD_DIR)/src && mkdir -p $(BUILD_DIR)/src
@for pkg in $(PACKAGES); do \
cp -a $(ROOT)/packages/$$pkg $(BUILD_DIR)/src/$$pkg ; \
( cd $(BUILD_DIR)/src/$$pkg && fakeroot debian/rules clean >/dev/null ) || exit 1 ; \
version=$$(dpkg-parsechangelog -l$(BUILD_DIR)/src/$$pkg/debian/changelog -SVersion) ; \
upstream=$${version#*:} ; upstream=$${upstream%-*} ; \
epoch=$$(date -u -d "$$(dpkg-parsechangelog -l$(BUILD_DIR)/src/$$pkg/debian/changelog -SDate)" +%s) ; \
tar --sort=name --mtime="@$$epoch" --owner=0 --group=0 --numeric-owner \
--exclude=$$pkg/debian --exclude=$$pkg/.git --exclude=$$pkg/.pc \
-C $(BUILD_DIR)/src -cJf $(BUILD_DIR)/src/$${pkg}_$${upstream}.orig.tar.xz $$pkg ; \
( cd $(BUILD_DIR)/src && dpkg-source -b $$pkg ) || exit 1 ; \
GNUPGHOME=$(BUILDER_HOME)/.gnupg debsign --no-conf -k$(REPO_KEY_ID) \
$(BUILD_DIR)/src/$${pkg}_$${version}.dsc || exit 1 ; \
retained=$(ROOT)/vendor/release-signatures/$${pkg}_$${version}.dsc ; \
if [ -f "$$retained" ]; then \
GNUPGHOME=$(BUILDER_HOME)/.gnupg python3 $(ROOT)/tools/retain_source_signature.py \
$(BUILD_DIR)/src/$${pkg}_$${version}.dsc "$$retained" || exit 1 ; \
fi ; \
rm -rf $(BUILD_DIR)/src/$$pkg ; \
$(REPREPRO) -b $(REPO_DIR) -T dsc remove $(CODENAME) $$pkg >/dev/null 2>&1 || true ; \
done
@for dsc in $(BUILD_DIR)/src/*.dsc; do \
$(REPREPRO) -b $(REPO_DIR) includedsc $(CODENAME) $$dsc ; \
done
@for deb in $(filter-out $(SHADOWCODE_DEB),$(RELEASE_DEBS)); do \
$(REPREPRO) -b $(REPO_DIR) includedeb $(CODENAME) $$deb ; \
done
# ShadowCode's control file has no Section, and reprepro skips such a package
# ("No section given") rather than guessing, so it is given one here -- the
# .deb bytes are untouched, only the index record carries Section: devel. It
# has no .dsc: nothing here builds it. Its release's runtime-sources tarball and
# signed metadata are published under pool/third-party-source/ instead, which
# the publisher uploads with the rest of pool/ (vendor/shadowcode/README.md).
@python3 $(SHADOWCODE_FETCH) --offline >/dev/null
@$(REPREPRO) -b $(REPO_DIR) -S devel -P optional includedeb $(CODENAME) $(SHADOWCODE_DEB) || exit 1
@set -o pipefail; python3 $(SHADOWCODE_FETCH) --offline --stage-sources $(REPO_DIR)/pool/third-party-source | tail -n 2
@tmp=$$(mktemp -d) ; \
trap 'rm -rf -- "$$tmp"' EXIT ; \
for deb in $(RELEASE_DEBS); do dpkg-deb -f "$$deb" Package; done | sort -u > "$$tmp/expected-binary" ; \
$(REPREPRO) -b $(REPO_DIR) list $(CODENAME) | awk '$$1 !~ /\|source:$$/ {print $$2}' | sort -u > "$$tmp/actual-binary" ; \
for dsc in $(BUILD_DIR)/src/*.dsc; do sed -n 's/^Source: //p' "$$dsc"; done | sort -u > "$$tmp/expected-source" ; \
$(REPREPRO) -b $(REPO_DIR) list $(CODENAME) | awk '$$1 ~ /\|source:$$/ {print $$2}' | sort -u > "$$tmp/actual-source" ; \
diff -u "$$tmp/expected-binary" "$$tmp/actual-binary" ; \
diff -u "$$tmp/expected-source" "$$tmp/actual-source" ; \
echo ">>> Repo allowlist passed: $$(wc -l < "$$tmp/actual-binary") binary, $$(wc -l < "$$tmp/actual-source") source packages"
@$(GPG) --armor --export $(REPO_KEY_ID) > $(REPO_DIR)/shadowfetch.gpg.asc
@echo ">>> Repo built at $(REPO_DIR). Contents:"
@$(REPREPRO) -b $(REPO_DIR) list $(CODENAME)
# Re-sign the repository indices in place: same pool, same packages, fresh
# Date/Valid-Until. This is what to run when the published metadata is
# approaching expiry -- it rebuilds no package and no ISO. Publish the
# resulting dists/ afterwards.
refresh-index:
@test -d $(REPO_DIR)/db || { echo "No repository database at $(REPO_DIR). Run 'make repo' first." >&2; exit 1; }
@test -d $(REPO_DIR)/pool || { echo "No pool at $(REPO_DIR)/pool; refusing to export an empty repository." >&2; exit 1; }
@before=$$($(REPREPRO) -b $(REPO_DIR) list $(CODENAME) | sort) ; \
printf '%s\n' \
'Origin: Shadowfetch' \
'Label: Shadowfetch' \
'Codename: $(CODENAME)' \
'Architectures: amd64 source' \
'Components: main' \
'Description: Shadowfetch Linux package repository' \
'ValidFor: $(REPO_VALID_FOR)' \
'DebIndices: Packages Release . .gz' \
'DscIndices: Sources Release . .gz' \
'SignWith: $(REPO_KEY_ID)' \
> $(REPO_DIR)/conf/distributions ; \
$(REPREPRO) -b $(REPO_DIR) export $(CODENAME) ; \
after=$$($(REPREPRO) -b $(REPO_DIR) list $(CODENAME) | sort) ; \
[ "$$before" = "$$after" ] || { echo 'Package list changed during a metadata refresh; refusing.' >&2; exit 1; }
@$(GPG) --armor --export $(REPO_KEY_ID) > $(REPO_DIR)/shadowfetch.gpg.asc
@$(MAKE) --no-print-directory check-index
@echo ">>> Indices re-signed in $(REPO_DIR)/dists. Publish dists/ (and shadowfetch.gpg.asc) to make it live."
# Fail if the local index is already expired or expires too soon. Runs on
# its own so it can be used as a monitoring check.
check-index:
@python3 -c 'import email.utils, sys, time; \
path = "$(REPO_DIR)/dists/$(CODENAME)/Release"; \
head = dict(l.split(": ", 1) for l in open(path) if ": " in l and not l.startswith(" ")); \
raw = head["Valid-Until"].strip(); \
left = email.utils.mktime_tz(email.utils.parsedate_tz(raw)) - time.time(); \
sys.stderr.write(">>> %s Valid-Until %s (%.1f days remaining)\n" % ("$(CODENAME)", raw, left / 86400.0)); \
sys.exit(0 if left > $(REPO_MIN_VALID_FOR_SECONDS) else 1)'
iso: repo
# A release build starts without any prior image or verification sidecars. If
# live-build fails later, no stale ISO can be mistaken for this invocation.
@rm -f $(ROOT)/$(ISO_NAME) $(ROOT)/$(ISO_NAME).sha256 $(ROOT)/$(ISO_NAME).asc
@rm -f $(LB_BUILD_LOG) $(LB_BUILD_MARKER) $(BUILD_DIR)/served-InRelease
# Stage a binary, dearmored GPG key for live-build's archives system.
@mkdir -p $(LB_DIR)/config/archives
@$(GPG) --dearmor < $(REPO_DIR)/shadowfetch.gpg.asc > $(LB_DIR)/config/archives/shadowfetch.key.chroot
@cp $(LB_DIR)/config/archives/shadowfetch.key.chroot $(LB_DIR)/config/archives/shadowfetch.key.binary
# W-19: the apt source entries name the repository key by path
# (signed-by=/usr/share/keyrings/shadowfetch.gpg) instead of trusting the
# transport with [trusted=yes], so that file has to exist in the chroot
# BEFORE live-build's first `apt-get update`, which runs inside
# lb_chroot_archives -- earlier than hooks, includes.chroot or any package
# install. config/archives/*.deb is the one live-build slot that installs
# into the chroot ahead of that update ("Check local keyring packages"),
# and the package it installs is also what carries the key into the
# squashfs for the installed system. Same key, same path, one source of
# truth: repo/shadowfetch.gpg.asc, which is what reprepro signs with.
@rm -rf $(BUILD_DIR)/archive-keyring
@mkdir -p $(BUILD_DIR)/archive-keyring/DEBIAN $(BUILD_DIR)/archive-keyring/usr/share/keyrings
@cp $(LB_DIR)/config/archives/shadowfetch.key.chroot $(BUILD_DIR)/archive-keyring/usr/share/keyrings/shadowfetch.gpg
@chmod 0644 $(BUILD_DIR)/archive-keyring/usr/share/keyrings/shadowfetch.gpg
@printf 'Package: shadowfetch-archive-keyring\nVersion: %s\nArchitecture: all\nSection: misc\nPriority: important\nMaintainer: Shadowfetch Project <signing@shadowfetch.com>\nDescription: GnuPG archive key for the Shadowfetch APT repository\n Ships /usr/share/keyrings/shadowfetch.gpg, the key named by signed-by=\n in every Shadowfetch apt source entry.\n' '$(VERSION)' > $(BUILD_DIR)/archive-keyring/DEBIAN/control
@dpkg-deb --root-owner-group --build $(BUILD_DIR)/archive-keyring $(LB_DIR)/config/archives/shadowfetch-archive-keyring.deb
# Clean the prior live-build output.
@cd $(LB_DIR) && sudo lb clean
# live-build caches archives by package name and version. During release QA,
# a same-version source correction must not silently reuse an older first-party
# .deb payload, so invalidate only Shadowfetch's locally built package cache.
@for cache in $(LB_DIR)/cache/packages.chroot $(LB_DIR)/cache/packages.binary; do \
if [ -d "$$cache" ]; then \
sudo find "$$cache" -maxdepth 1 -type f \
\( -name 'shadowfetch-*.deb' -o -name 'grub-btrfs_*.deb' \) -delete ; \
fi ; \
done
@test ! -e $(LB_DIR)/binary/live/filesystem.squashfs || { echo "FATAL: lb clean left a stale squashfs" >&2; exit 1; }
@touch $(LB_BUILD_MARKER)
# Serve the repo to the chroot. The old live-build binary_grub2 stage is not
# usable here, so the final hybrid image is assembled with grub-mkrescue below.
@bash -c 'set -euo pipefail ; \
cd "$(REPO_DIR)" ; \
python3 -m http.server $(LOCAL_REPO_PORT) --bind 127.0.0.1 >"$(BUILD_DIR)/repo-server.log" 2>&1 & \
SERVER_PID=$$! ; \
cleanup() { kill "$$SERVER_PID" 2>/dev/null || true; wait "$$SERVER_PID" 2>/dev/null || true; } ; \
trap cleanup EXIT INT TERM ; \
sleep 1 ; \
if ! kill -0 "$$SERVER_PID" 2>/dev/null; then cat "$(BUILD_DIR)/repo-server.log" >&2; echo "FATAL: local repository server did not start" >&2; exit 1; fi ; \
curl --fail --silent --show-error --max-time 10 "http://127.0.0.1:$(LOCAL_REPO_PORT)/dists/$(CODENAME)/InRelease" > "$(BUILD_DIR)/served-InRelease" ; \
cmp "$(BUILD_DIR)/served-InRelease" "$(REPO_DIR)/dists/$(CODENAME)/InRelease" ; \
rm -f "$(BUILD_DIR)/served-InRelease" ; \
echo ">>> Verified local repo server PID=$$SERVER_PID on :$(LOCAL_REPO_PORT)" ; \
cd "$(LB_DIR)" ; \
sudo lb config ; \
sudo lb build 2>&1 | tee "$(LB_BUILD_LOG)"'
# Take ownership of live-build output and copy its kernel artifacts explicitly.
@sudo chown -R $(BUILDER_USER):$(BUILDER_GROUP) $(LB_DIR)/binary
@sudo chmod -R a+rX $(LB_DIR)/chroot/boot
# The squashfs is the first hard image gate.
@test -f $(LB_DIR)/binary/live/filesystem.squashfs || { echo "FATAL: no squashfs in $(LB_DIR)/binary/live/" >&2; exit 1; }
@test $(LB_DIR)/binary/live/filesystem.squashfs -nt $(LB_BUILD_MARKER) || { echo "FATAL: squashfs is not fresh for this build" >&2; exit 1; }
# Copy the newest kernel and initrd under predictable names.
@KERNEL=$$(ls $(LB_DIR)/chroot/boot/vmlinuz-*-amd64 2>/dev/null | sort -V | tail -1) ; \
INITRD=$$(ls $(LB_DIR)/chroot/boot/initrd.img-*-amd64 2>/dev/null | sort -V | tail -1) ; \
[ -n "$$KERNEL" ] || { echo "FATAL: no kernel in $(LB_DIR)/chroot/boot/" >&2; exit 1; } ; \
[ -n "$$INITRD" ] || { echo "FATAL: no initrd in $(LB_DIR)/chroot/boot/" >&2; exit 1; } ; \
echo ">>> Found kernel: $$KERNEL" ; \
echo ">>> Found initrd: $$INITRD" ; \
cp "$$KERNEL" $(LB_DIR)/binary/live/vmlinuz ; \
cp "$$INITRD" $(LB_DIR)/binary/live/initrd.img
# Replace live-build's GRUB configuration.
@mkdir -p $(LB_DIR)/binary/boot/grub
@cp $(ROOT)/live-build/config/grub.cfg $(LB_DIR)/binary/boot/grub/grub.cfg
# Install the Umbra GRUB theme.
@mkdir -p $(LB_DIR)/binary/boot/grub/themes
@cp -r $(ROOT)/live-build/config/grub-theme/umbra $(LB_DIR)/binary/boot/grub/themes/umbra
# live-build created SHA256SUMS before the kernel, initrd and final GRUB files
# above were replaced. Regenerate it over the exact tree sent to grub-mkrescue.
@cd $(LB_DIR)/binary && \
find . -type f ! -name SHA256SUMS -print0 | LC_ALL=C sort -z | \
xargs -0 sha256sum > SHA256SUMS
@cd $(LB_DIR)/binary && sha256sum --check --quiet SHA256SUMS
# Assemble the final BIOS and UEFI hybrid image.
@echo ">>> Assembling final ISO with grub-mkrescue (BIOS + UEFI hybrid)"
@rm -f $(ROOT)/$(ISO_NAME)
@sudo grub-mkrescue \
--output=$(ROOT)/$(ISO_NAME) \
$(LB_DIR)/binary \
-- -volid SHADOWFETCH
@sudo chown $(BUILDER_USER):$(BUILDER_GROUP) $(ROOT)/$(ISO_NAME)
@test $(ROOT)/$(ISO_NAME) -nt $(LB_BUILD_MARKER) || { echo "FATAL: ISO is not fresh for this build" >&2; exit 1; }
@echo ">>> Built $(ISO_NAME)"
@ls -lh $(ROOT)/$(ISO_NAME)
@cd $(ROOT) && sha256sum $(ISO_NAME) > $(ISO_NAME).sha256
@cd $(ROOT) && sha256sum --check $(ISO_NAME).sha256
@echo ">>> SHA256: $$(cat $(ROOT)/$(ISO_NAME).sha256)"
@$(MAKE) sign
@$(MAKE) iso-gate
iso-gate: release-data
@mkdir -p $(dir $(ISO_GATE_LOG))
@test -x $(ISO_GATE) || { echo "Missing ISO gate: $(ISO_GATE)" >&2; exit 1; }
@set -o pipefail; $(ISO_GATE) --version $(VERSION) 2>&1 | tee $(ISO_GATE_LOG)
# Detached GPG signature so downloaders can verify with: gpg --verify <iso>.asc
sign:
@if [ ! -f $(ROOT)/$(ISO_NAME) ]; then echo "No ISO at $(ROOT)/$(ISO_NAME) — run 'make iso' first" >&2; exit 1; fi
@rm -f $(ROOT)/$(ISO_NAME).asc
@$(GPG) --batch --yes --local-user $(REPO_KEY_ID) --armor --detach-sign --output $(ROOT)/$(ISO_NAME).asc $(ROOT)/$(ISO_NAME)
@$(GPG) --batch --verify $(ROOT)/$(ISO_NAME).asc $(ROOT)/$(ISO_NAME)
@echo ">>> Signed: $(ROOT)/$(ISO_NAME).asc"
# Validate release metadata and local repo hygiene before any publication.
pre-release-check: iso-gate
@ROOT=$(ROOT) CODENAME=$(CODENAME) REPO_DIR=$(REPO_DIR) REPO_MIN_VALID_FOR_SECONDS=$(REPO_MIN_VALID_FOR_SECONDS) \
$(ROOT)/tools/pre_release_check.sh
# Publish only the accepted artifact from the authorized Linux source tree.
# The publisher preserves historical ISO/package objects, verifies signatures,
# and writes signed APT InRelease last. Credentials are process environment only.
publish: pre-release-check
@python3 $(ROOT)/tools/publish_release_4_0_0.py --apply
qemu:
qemu-system-x86_64 \
-enable-kvm \
-cpu host -smp 4 -m 8192 \
-vga virtio -display gtk,gl=on \
-device intel-hda -device hda-duplex \
-net nic,model=virtio -net user \
-drive file=$(ROOT)/$(ISO_NAME),media=cdrom,readonly=on \
-boot d
# Pull a read-only local copy of already built release artifacts for inspection.
sync-from-linux:
@rsync -avzhP $(LINUX_HOST):$(LINUX_PATH)/$(ISO_NAME) $(ROOT)/
@rsync -avzhP $(LINUX_HOST):$(LINUX_PATH)/$(ISO_NAME).sha256 $(ROOT)/
@rsync -avzhP $(LINUX_HOST):$(LINUX_PATH)/$(ISO_NAME).asc $(ROOT)/
# Artifact routing is stable across distro releases. Deploying a Worker is a
# separate reviewed change, never an implicit side effect of publishing an ISO.
deploy-worker:
@echo "Artifact routing is unchanged. Review any Worker change separately." >&2
@exit 1
ship: publish
@echo ">>> Accepted artifacts published. Complete public byte verification, GitHub release, and canonical Linux website deployment per RELEASE-$(VERSION).md."
clean:
-cd $(LB_DIR) && sudo lb clean
-rm -rf $(BUILD_DIR)
-rm -f $(LB_DIR)/config/archives/shadowfetch.key.*
-rm -f $(LB_DIR)/config/archives/shadowfetch-archive-keyring.deb
-rm -rf $(BUILD_DIR)/archive-keyring
distclean: clean
-rm -rf $(REPO_DIR)/db $(REPO_DIR)/dists $(REPO_DIR)/pool $(REPO_DIR)/conf
-rm -f $(REPO_DIR)/shadowfetch.gpg.asc
-rm -f $(ROOT)/*.iso $(ROOT)/*.iso.sha256