Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Makes wheel builds reproducible, with tests #211

Merged
merged 7 commits into from
Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,14 +281,15 @@ common-steps:
version: 2.1
jobs:
tests:
docker:
- image: circleci/python:3.7-buster
machine:
image: ubuntu-2004:202010-01
steps:
- checkout
- run:
name: install test requirements and run tests
command: |
virtualenv .venv
make install-deps
virtualenv -p python3 .venv
source .venv/bin/activate
pip install -r test-requirements.txt
make test
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
tests/__pycache__/
debhelper-build-stamp
*.debhelper.log
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ test: ## Run test suite
clean: ## Removes all non-version controlled packaging artifacts
rm -rf localwheels/*

.PHONY: reprotest
reprotest: ## Reproducibility test, currently only for wheels
pytest -vvs tests/test_reproducible_wheels.py

.PHONY: help
help: ## Prints this message and exits
@printf "Makefile for building SecureDrop Workstation packages\n"
Expand Down
62 changes: 51 additions & 11 deletions scripts/build-sync-wheels
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,56 @@

import os
import sys
import json
import subprocess
import tempfile
import shutil
import argparse


# Set SOURCE_DATE_EPOCH to a predictable value. Using the first
# commit to the SecureDrop project:
#
# git show -s 62bbe590afd77a6af2dcaed46c93da6e0cf40951 --date=unix
#
# which yields 1309379017
os.environ["SOURCE_DATE_EPOCH"] = "1309379017"

# Force sane umask for reproducibility's sake.
os.umask(0o022)

# When building wheels, pip defaults to a safe dynamic tmpdir path.
# Some shared objects include the path from build, so we must make
# the path predictable across builds.
WHEEL_BUILD_DIR = "/tmp/pip-wheel-build"


def main():
parser = argparse.ArgumentParser(
description="Builds and caches sources and wheels"
)
parser.add_argument(
"-p",
help="Points to the project dirctory",
help="Points to the project directory",
)
parser.add_argument(
"--cache", default="./localwheels", help="Final cache dir"
)
parser.add_argument(
"--clobber", action="store_true", default=False,
help="Whether to overwrite wheels and source tarballs",
)
args = parser.parse_args()

if not os.path.exists(args.p):
print("Project directory missing {0}.".format(args.p))
sys.exit(1)
if args.p.startswith("https://"):
git_clone_directory = tempfile.mkdtemp(prefix=os.path.basename(args.p))
cmd = f"git clone {args.p} {git_clone_directory}".split()
subprocess.check_call(cmd)
args.p = git_clone_directory
else:
git_clone_directory = ""
if not os.path.exists(args.p):
print("Project directory missing {0}.".format(args.p))
sys.exit(1)

# Try requirements.txt in the repo root, otherwise try requirements/requirements.txt
req_path = os.path.join(args.p, "requirements.txt")
Expand All @@ -36,6 +63,11 @@ def main():
print("requirements.txt missing at {0}.".format(req_path))
sys.exit(3)

if os.path.exists(WHEEL_BUILD_DIR):
shutil.rmtree(WHEEL_BUILD_DIR)
else:
os.mkdir(WHEEL_BUILD_DIR)

with tempfile.TemporaryDirectory() as tmpdir:
# The --require-hashes option will be used by default if there are
# hashes in the requirements.txt file. We specify it anyway to guard
Expand All @@ -46,9 +78,9 @@ def main():
"--no-binary",
":all:",
"--require-hashes",
"-d",
"--dest",
tmpdir,
"-r",
"--requirement",
req_path,
]
subprocess.check_call(cmd)
Expand All @@ -58,11 +90,15 @@ def main():
"wheel",
"--no-binary",
":all:",
"-f",
"--find-links",
tmpdir,
"-w",
"--progress-bar",
"pretty",
"--wheel-dir",
tmpdir,
"-r",
"--build",
WHEEL_BUILD_DIR,
"--requirement",
req_path,
]
subprocess.check_call(cmd)
Expand All @@ -76,13 +112,17 @@ def main():
if name == "requirements.txt": # We don't need this in cache
continue
if name in cachenames: # Means all ready in our cache
continue
if not args.clobber:
continue

# Else copy to cache
filepath = os.path.join(tmpdir, name)
shutil.copy(filepath, args.cache, follow_symlinks=True)
print("Copying {0} to cache {1}".format(name, args.cache))

if git_clone_directory:
shutil.rmtree(git_clone_directory)


if __name__ == "__main__":
main()
8 changes: 7 additions & 1 deletion scripts/install-deps
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
# Installs required dependencies for building SecureDrop Worsktation packages.
# Assumes a Debian 10 machine, ideally a Qubes AppVM.

# If running in CI, we need to add the Ubuntu Bionic repo to download dh-virtualenv
if [[ -v CIRCLE_BUILD_URL ]]; then
echo "deb http://archive.ubuntu.com/ubuntu/ bionic universe" | sudo tee -a /etc/apt/sources.list
fi

sudo apt-get update
sudo apt-get install \
build-essential \
Expand All @@ -15,7 +20,8 @@ sudo apt-get install \
libssl-dev \
python3-all \
python3-pip \
python3-setuptools
python3-setuptools \
reprotest


# Inspect the wheel files present locally. If repo was cloned
Expand Down
43 changes: 43 additions & 0 deletions tests/test_reproducible_wheels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import pytest
import subprocess


# These are the SDW repositories that we build wheels for.
REPOS_WITH_WHEELS = [
"securedrop-client",
"securedrop-log",
"securedrop-proxy",
]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there other repositories for which we build wheels, that should be added to this list?



def get_repo_root():
cmd = "git rev-parse --show-toplevel".split()
top_level = subprocess.check_output(cmd).decode("utf-8").rstrip()
return top_level


@pytest.mark.parametrize("repo_name", REPOS_WITH_WHEELS)
def test_wheel_builds_are_reproducible(repo_name):
"""
Uses 'reprotest' to confirm that the wheel build process, per repo,
is deterministic, i.e. all .whl files are created with the same checksum
across multiple builds.

Explanations of the excluded reproducibility checks:

* time: breaks HTTPS, so pip calls fail
* locales: some locales fail, would be nice to fix, but low priority
* kernel: x86_64 is the supported architecure, we don't ship others
"""
repo_url = f"https://github.com/freedomofpress/{repo_name}"
cmd = [
"reprotest",
"-c",
f"./scripts/build-sync-wheels -p {repo_url} --clobber",
"--vary",
"+all, -time, -locales, -kernel",
".",
"localwheels/*.whl",
]
repo_root = get_repo_root()
subprocess.check_call(cmd, cwd=repo_root)