Skip to content

Commit e7de309

Browse files
committed
Calculate checksums of native deps and auto-update in repo.
1 parent 9604962 commit e7de309

File tree

2 files changed

+323
-8
lines changed

2 files changed

+323
-8
lines changed

.github/workflows/deps.yaml

+155-8
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,25 @@ jobs:
2323
- name: Check out repo
2424
uses: actions/checkout@v4
2525

26+
- name: Cache build tools
27+
uses: actions/cache@v3
28+
with:
29+
path: |
30+
/var/cache/apt
31+
/var/lib/apt
32+
key: ${{ runner.os }}-build-tools-${{ hashFiles('deps/build-deps-linux.sh') }}
33+
2634
- name: Install build tools
2735
run: |
2836
sudo apt-get update
29-
sudo apt-get install nasm
37+
sudo apt-get install nasm python3
3038
3139
- name: Build deps
3240
run: |
41+
echo "Starting dependency build..."
3342
export MAKEFLAGS="-j$(nproc)"
3443
./deps/build-deps-linux.sh
44+
echo "Dependency build completed"
3545
3646
- run: |
3747
git status
@@ -48,14 +58,22 @@ jobs:
4858
go build
4959
go test -v
5060
51-
- name: Compress deps
52-
run: tar -czf deps.tar.gz deps/linux
61+
- name: Generate build info
62+
run: |
63+
./deps/verify_deps.py generate \
64+
--deps-dir deps/linux \
65+
--platform linux \
66+
--commit ${{ github.sha }}
67+
68+
- name: Create deps archive
69+
run: |
70+
tar -czf deps-linux.tar.gz deps/linux/
5371
5472
- name: Upload deps artifact
5573
uses: actions/upload-artifact@v4
5674
with:
5775
name: deps-linux.tar.gz
58-
path: deps.tar.gz
76+
path: deps-linux.tar.gz
5977

6078
macos:
6179
name: macOS
@@ -64,6 +82,16 @@ jobs:
6482
- name: Check out repo
6583
uses: actions/checkout@v4
6684

85+
- name: Cache Homebrew packages
86+
uses: actions/cache@v3
87+
with:
88+
path: |
89+
~/Library/Caches/Homebrew
90+
~/Library/Logs/Homebrew
91+
key: ${{ runner.os }}-brew-${{ hashFiles('deps/build-deps-osx.sh') }}
92+
restore-keys: |
93+
${{ runner.os }}-brew-
94+
6795
- name: Install build tools
6896
run: |
6997
brew install autoconf
@@ -73,8 +101,10 @@ jobs:
73101
74102
- name: Build deps
75103
run: |
104+
echo "Starting dependency build..."
76105
export MAKEFLAGS="-j$(nproc)"
77106
./deps/build-deps-osx.sh
107+
echo "Dependency build completed"
78108
79109
- run: |
80110
git status
@@ -90,12 +120,129 @@ jobs:
90120
run: |
91121
go build
92122
go test -v
93-
94-
- name: Compress deps
95-
run: tar -czf deps.tar.gz deps/osx
123+
124+
- name: Generate build info
125+
run: |
126+
./deps/verify_deps.py generate \
127+
--deps-dir deps/osx \
128+
--platform macos \
129+
--commit ${{ github.sha }}
130+
131+
- name: Create deps archive
132+
run: |
133+
tar -czf deps-macos.tar.gz deps/osx/
96134
97135
- name: Upload deps artifact
98136
uses: actions/upload-artifact@v4
99137
with:
100138
name: deps-macos.tar.gz
101-
path: deps.tar.gz
139+
path: deps-macos.tar.gz
140+
141+
verify:
142+
name: Verify Build Artifacts
143+
needs: [linux, macos]
144+
runs-on: ubuntu-latest
145+
if: github.event_name == 'pull_request'
146+
147+
steps:
148+
- name: Check out repo
149+
uses: actions/checkout@v4
150+
151+
- name: Download Linux artifact
152+
uses: actions/download-artifact@v4
153+
with:
154+
name: deps-linux.tar.gz
155+
path: .
156+
157+
- name: Download macOS artifact
158+
uses: actions/download-artifact@v4
159+
with:
160+
name: deps-macos.tar.gz
161+
path: .
162+
163+
- name: Extract artifacts
164+
run: |
165+
tar xzf deps-linux.tar.gz
166+
tar xzf deps-macos.tar.gz
167+
168+
- name: Verify artifacts match checked-in deps
169+
run: |
170+
python3 ./deps/verify_deps.py verify \
171+
--deps-dir deps/linux \
172+
--build-info deps/linux/build-info.json
173+
174+
python3 ./deps/verify_deps.py verify \
175+
--deps-dir deps/osx \
176+
--build-info deps/osx/build-info.json
177+
178+
update-deps:
179+
name: Update Checked-in Dependencies
180+
needs: [linux, macos]
181+
runs-on: ubuntu-latest
182+
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
183+
timeout-minutes: 30
184+
185+
steps:
186+
- name: Check out repo
187+
uses: actions/checkout@v4
188+
189+
- name: Download Linux artifact
190+
uses: actions/download-artifact@v4
191+
with:
192+
name: deps-linux.tar.gz
193+
path: .
194+
195+
- name: Download macOS artifact
196+
uses: actions/download-artifact@v4
197+
with:
198+
name: deps-macos.tar.gz
199+
path: .
200+
201+
- name: Extract and update deps
202+
run: |
203+
# Remove existing deps directories to avoid stale files
204+
rm -rf deps/linux/* deps/osx/*
205+
206+
tar xzf deps-linux.tar.gz
207+
tar xzf deps-macos.tar.gz
208+
209+
- name: Commit updated deps
210+
run: |
211+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
212+
git config --local user.name "github-actions[bot]"
213+
git config --local commit.gpgsign false
214+
215+
# Stage each type of file explicitly
216+
shopt -s nullglob # Handle case where globs don't match
217+
218+
# Binary files
219+
for f in deps/*/lib/*.{so,so.*,dylib,dylib.*,a}; do
220+
if [ -f "$f" ]; then
221+
git add -f "$f"
222+
fi
223+
done
224+
225+
# Header files
226+
for f in deps/*/include/**/*.h; do
227+
if [ -f "$f" ]; then
228+
git add -f "$f"
229+
fi
230+
done
231+
232+
# Build info
233+
for f in deps/*/build-info.json; do
234+
if [ -f "$f" ]; then
235+
git add -f "$f"
236+
fi
237+
done
238+
239+
# Only commit if there are changes
240+
if ! git diff --cached --quiet; then
241+
git commit -m "Update native dependencies from ${{ github.sha }} [skip ci]
242+
243+
Dependencies built by workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
244+
245+
git push
246+
else
247+
echo "No changes to checked-in dependencies"
248+
fi

deps/verify_deps.py

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import hashlib
4+
import json
5+
import os
6+
import sys
7+
from pathlib import Path
8+
from typing import Dict, List, NamedTuple, Tuple
9+
10+
11+
class BuildInfo(NamedTuple):
12+
commit_sha: str
13+
platform: str
14+
files: Dict[str, str] # relative path -> sha256
15+
16+
17+
def calculate_checksum(file_path: Path) -> str:
18+
"""Calculate SHA-256 checksum of a file."""
19+
sha256_hash = hashlib.sha256()
20+
with open(file_path, "rb") as f:
21+
# Read in 1MB chunks to handle large files efficiently
22+
for byte_block in iter(lambda: f.read(4096*256), b""):
23+
sha256_hash.update(byte_block)
24+
return sha256_hash.hexdigest()
25+
26+
27+
def scan_deps(deps_dir: Path) -> Dict[str, str]:
28+
"""Scan directory for dependency files and calculate their checksums."""
29+
checksums = {}
30+
for file_path in deps_dir.rglob("*"):
31+
if not file_path.is_file():
32+
continue
33+
34+
# Only process shared and static libraries and headers
35+
if not file_path.suffix in ['.so', '.dylib', '.a', '.h']:
36+
continue
37+
38+
# Get path relative to deps_dir
39+
rel_path = str(file_path.relative_to(deps_dir))
40+
try:
41+
checksums[rel_path] = calculate_checksum(file_path)
42+
except (IOError, OSError) as e:
43+
print(f"Error processing {rel_path}: {e}", file=sys.stderr)
44+
continue
45+
46+
return checksums
47+
48+
49+
def generate_build_info(deps_dir: Path, platform: str, commit_sha: str) -> BuildInfo:
50+
"""Generate build info for the given deps directory."""
51+
checksums = scan_deps(deps_dir)
52+
return BuildInfo(
53+
commit_sha=commit_sha,
54+
platform=platform,
55+
files=checksums
56+
)
57+
58+
59+
def verify_deps(deps_dir: Path, build_info: BuildInfo) -> Tuple[bool, List[str]]:
60+
"""Verify deps directory against build info."""
61+
mismatches = []
62+
valid = True
63+
64+
# Get current state of deps directory
65+
current_checksums = scan_deps(deps_dir)
66+
67+
print(f"Found {len(current_checksums)} files to verify")
68+
69+
# Check for missing or mismatched files
70+
for rel_path, expected_checksum in build_info.files.items():
71+
if rel_path not in current_checksums:
72+
mismatches.append(f"{rel_path}: file not found in deps directory")
73+
valid = False
74+
continue
75+
76+
actual_checksum = current_checksums[rel_path]
77+
if actual_checksum != expected_checksum:
78+
mismatches.append(
79+
f"{rel_path}: checksum mismatch\n"
80+
f" expected: {expected_checksum}\n"
81+
f" got: {actual_checksum}"
82+
)
83+
valid = False
84+
else:
85+
print(f"Verified: {rel_path}")
86+
87+
# Check for extra files
88+
for rel_path in current_checksums:
89+
if rel_path not in build_info.files:
90+
mismatches.append(f"{rel_path}: extra file in deps directory")
91+
valid = False
92+
93+
return valid, mismatches
94+
95+
96+
def main():
97+
parser = argparse.ArgumentParser(description="Verify Lilliput dependencies")
98+
99+
# Create subparsers first
100+
subparsers = parser.add_subparsers(dest="command", required=True)
101+
102+
# Generate command
103+
generate_parser = subparsers.add_parser("generate",
104+
help="Generate build info for dependencies")
105+
generate_parser.add_argument("--deps-dir", required=True, type=Path,
106+
help="Directory containing dependencies")
107+
generate_parser.add_argument("--platform", required=True,
108+
choices=["linux", "macos"],
109+
help="Platform identifier")
110+
generate_parser.add_argument("--commit", required=True,
111+
help="Commit SHA that produced the build")
112+
generate_parser.add_argument("--output", type=Path,
113+
help="Output file (default: <deps-dir>/build-info.json)")
114+
115+
# Verify command
116+
verify_parser = subparsers.add_parser("verify",
117+
help="Verify deps against build info")
118+
verify_parser.add_argument("--deps-dir", required=True, type=Path,
119+
help="Directory containing dependencies")
120+
verify_parser.add_argument("--build-info", required=True, type=Path,
121+
help="Path to build info JSON file")
122+
123+
args = parser.parse_args()
124+
125+
if not os.path.exists(args.deps_dir):
126+
print(f"Error: deps directory not found: {args.deps_dir}", file=sys.stderr)
127+
sys.exit(1)
128+
129+
if args.command == "generate":
130+
build_info = generate_build_info(
131+
args.deps_dir,
132+
args.platform,
133+
args.commit
134+
)
135+
136+
output_file = args.output or args.deps_dir / "build-info.json"
137+
138+
try:
139+
with open(output_file, "w") as f:
140+
json.dump(build_info._asdict(), f, indent=4)
141+
print(f"Build info generated successfully: {output_file}")
142+
except (IOError, OSError) as e:
143+
print(f"Error writing build info: {e}", file=sys.stderr)
144+
sys.exit(1)
145+
146+
elif args.command == "verify":
147+
try:
148+
with open(args.build_info) as f:
149+
build_info_dict = json.load(f)
150+
build_info = BuildInfo(**build_info_dict)
151+
except (IOError, OSError, json.JSONDecodeError) as e:
152+
print(f"Error reading build info: {e}", file=sys.stderr)
153+
sys.exit(1)
154+
155+
print(f"Verifying deps against build from commit {build_info.commit_sha}")
156+
valid, mismatches = verify_deps(args.deps_dir, build_info)
157+
158+
if not valid:
159+
print("\nVerification failed:")
160+
for mismatch in mismatches:
161+
print(f" {mismatch}")
162+
sys.exit(1)
163+
164+
print("\nAll dependencies verified successfully")
165+
166+
167+
if __name__ == "__main__":
168+
main()

0 commit comments

Comments
 (0)