Skip to content

Commit 879db97

Browse files
authored
Merge pull request #1 from skidder/skidder/auto-update-native-deps
Calculate checksums of native deps and auto-update in repo.
2 parents 9604962 + e4685fc commit 879db97

File tree

2 files changed

+278
-8
lines changed

2 files changed

+278
-8
lines changed

.github/workflows/deps.yaml

+114-8
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
- name: Install build tools
2727
run: |
2828
sudo apt-get update
29-
sudo apt-get install nasm
29+
sudo apt-get install nasm python3
3030
3131
- name: Build deps
3232
run: |
@@ -48,14 +48,22 @@ jobs:
4848
go build
4949
go test -v
5050
51-
- name: Compress deps
52-
run: tar -czf deps.tar.gz deps/linux
51+
- name: Generate build info
52+
run: |
53+
./verify_deps.py generate \
54+
--deps-dir deps/linux \
55+
--platform linux \
56+
--commit ${{ github.sha }}
57+
58+
- name: Create deps archive
59+
run: |
60+
tar -czf deps-linux.tar.gz deps/linux/
5361
5462
- name: Upload deps artifact
5563
uses: actions/upload-artifact@v4
5664
with:
5765
name: deps-linux.tar.gz
58-
path: deps.tar.gz
66+
path: deps-linux.tar.gz
5967

6068
macos:
6169
name: macOS
@@ -70,6 +78,7 @@ jobs:
7078
brew install automake
7179
brew install coreutils # for ffmpeg build
7280
brew install libtool
81+
python3 -m pip install --user --upgrade pip
7382
7483
- name: Build deps
7584
run: |
@@ -90,12 +99,109 @@ jobs:
9099
run: |
91100
go build
92101
go test -v
93-
94-
- name: Compress deps
95-
run: tar -czf deps.tar.gz deps/osx
102+
103+
- name: Generate build info
104+
run: |
105+
./verify_deps.py generate \
106+
--deps-dir deps/osx \
107+
--platform macos \
108+
--commit ${{ github.sha }}
109+
110+
- name: Create deps archive
111+
run: |
112+
tar -czf deps-macos.tar.gz deps/osx/
96113
97114
- name: Upload deps artifact
98115
uses: actions/upload-artifact@v4
99116
with:
100117
name: deps-macos.tar.gz
101-
path: deps.tar.gz
118+
path: deps-macos.tar.gz
119+
120+
verify:
121+
name: Verify Build Artifacts
122+
needs: [linux, macos]
123+
runs-on: ubuntu-latest
124+
# Only run verification on PRs to avoid duplicate runs on pushes to main
125+
if: github.event_name == 'pull_request'
126+
127+
steps:
128+
- name: Check out repo
129+
uses: actions/checkout@v4
130+
131+
- name: Download Linux artifact
132+
uses: actions/download-artifact@v4
133+
with:
134+
name: deps-linux.tar.gz
135+
path: .
136+
137+
- name: Download macOS artifact
138+
uses: actions/download-artifact@v4
139+
with:
140+
name: deps-macos.tar.gz
141+
path: .
142+
143+
- name: Extract artifacts
144+
run: |
145+
tar xzf deps-linux.tar.gz
146+
tar xzf deps-macos.tar.gz
147+
148+
- name: Verify artifacts match checked-in deps
149+
run: |
150+
python3 ./verify_deps.py verify-artifact \
151+
--artifact deps-linux.tar.gz \
152+
--deps-dir deps/linux
153+
154+
python3 ./verify_deps.py verify-artifact \
155+
--artifact deps-macos.tar.gz \
156+
--deps-dir deps/osx
157+
158+
# This job updates the checked-in deps on main branch
159+
update-deps:
160+
name: Update Checked-in Dependencies
161+
needs: [linux, macos]
162+
runs-on: ubuntu-latest
163+
# Only run on pushes to main, not on PRs
164+
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
165+
166+
steps:
167+
- name: Check out repo
168+
uses: actions/checkout@v4
169+
170+
- name: Download Linux artifact
171+
uses: actions/download-artifact@v4
172+
with:
173+
name: deps-linux.tar.gz
174+
path: .
175+
176+
- name: Download macOS artifact
177+
uses: actions/download-artifact@v4
178+
with:
179+
name: deps-macos.tar.gz
180+
path: .
181+
182+
- name: Extract and update deps
183+
run: |
184+
# Remove existing deps directories to avoid stale files
185+
rm -rf deps/linux/* deps/osx/*
186+
187+
tar xzf deps-linux.tar.gz
188+
tar xzf deps-macos.tar.gz
189+
190+
- name: Commit updated deps
191+
run: |
192+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
193+
git config --local user.name "github-actions[bot]"
194+
195+
# Force add the deps directories to override gitignore
196+
git add -f deps/linux/* deps/osx/*
197+
198+
# Only commit if there are changes
199+
if ! git diff --cached --quiet; then
200+
git commit -m "Update native dependencies from ${{ github.sha }} [skip ci]
201+
202+
Dependencies built by workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
203+
204+
git push
205+
else
206+
echo "No changes to checked-in dependencies"
207+
fi

deps/verify_deps.py

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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
35+
if not file_path.suffix in ['.so', '.dylib', '.a']:
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+
# Check for missing or mismatched files
68+
for rel_path, expected_checksum in build_info.files.items():
69+
if rel_path not in current_checksums:
70+
mismatches.append(f"{rel_path}: file not found in deps directory")
71+
valid = False
72+
continue
73+
74+
actual_checksum = current_checksums[rel_path]
75+
if actual_checksum != expected_checksum:
76+
mismatches.append(
77+
f"{rel_path}: checksum mismatch\n"
78+
f" expected: {expected_checksum}\n"
79+
f" got: {actual_checksum}"
80+
)
81+
valid = False
82+
83+
# Check for extra files
84+
for rel_path in current_checksums:
85+
if rel_path not in build_info.files:
86+
mismatches.append(f"{rel_path}: extra file in deps directory")
87+
valid = False
88+
89+
return valid, mismatches
90+
91+
92+
def main():
93+
parser = argparse.ArgumentParser(description="Verify Lilliput dependencies")
94+
parser.add_argument("--deps-dir", required=True, type=Path,
95+
help="Directory containing dependencies (e.g., deps/linux or deps/osx)")
96+
97+
subparsers = parser.add_subparsers(dest="command", required=True)
98+
99+
# Generate command
100+
generate_parser = subparsers.add_parser("generate",
101+
help="Generate build info for dependencies")
102+
generate_parser.add_argument("--platform", required=True,
103+
choices=["linux", "macos"],
104+
help="Platform identifier")
105+
generate_parser.add_argument("--commit", required=True,
106+
help="Commit SHA that produced the build")
107+
generate_parser.add_argument("--output", type=Path,
108+
help="Output file (default: <deps-dir>/build-info.json)")
109+
110+
# Verify command
111+
verify_parser = subparsers.add_parser("verify",
112+
help="Verify deps against build info")
113+
verify_parser.add_argument("--build-info", required=True, type=Path,
114+
help="Path to build info JSON file")
115+
116+
args = parser.parse_args()
117+
118+
if not args.deps_dir.exists():
119+
print(f"Error: deps directory not found: {args.deps_dir}", file=sys.stderr)
120+
sys.exit(1)
121+
122+
if args.command == "generate":
123+
build_info = generate_build_info(
124+
args.deps_dir,
125+
args.platform,
126+
args.commit
127+
)
128+
129+
output_file = args.output or args.deps_dir / "build-info.json"
130+
131+
# Convert BuildInfo to dict for JSON serialization
132+
build_info_dict = build_info._asdict()
133+
134+
try:
135+
with open(output_file, "w") as f:
136+
json.dump(build_info_dict, f, indent=4)
137+
print(f"Build info generated successfully: {output_file}")
138+
except (IOError, OSError) as e:
139+
print(f"Error writing build info: {e}", file=sys.stderr)
140+
sys.exit(1)
141+
142+
elif args.command == "verify":
143+
try:
144+
with open(args.build_info) as f:
145+
build_info_dict = json.load(f)
146+
build_info = BuildInfo(**build_info_dict)
147+
except (IOError, OSError, json.JSONDecodeError) as e:
148+
print(f"Error reading build info: {e}", file=sys.stderr)
149+
sys.exit(1)
150+
151+
print(f"Verifying deps against build from commit {build_info.commit_sha}")
152+
valid, mismatches = verify_deps(args.deps_dir, build_info)
153+
154+
if not valid:
155+
print("\nVerification failed:")
156+
for mismatch in mismatches:
157+
print(f" {mismatch}")
158+
sys.exit(1)
159+
160+
print("\nAll dependencies verified successfully")
161+
162+
163+
if __name__ == "__main__":
164+
main()

0 commit comments

Comments
 (0)