Skip to content
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
75 changes: 47 additions & 28 deletions bin/check_release
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
import ast
import os
import re
import subprocess
import subprocess # nosec
import sys
from typing import cast
from typing import List
from typing import Optional
from typing import List, Optional, cast

import requests

Expand All @@ -15,10 +13,14 @@ def github_repo() -> str:
repo = os.environ.get("GITHUB_REPOSITORY")
if repo:
return repo
return (subprocess.run(
["git", "remote", "get-url", "upstream"],
check=True,
capture_output=True).stdout.decode("utf-8").strip().split(":")[1])
return (
subprocess.run(
["git", "remote", "get-url", "upstream"], check=True, capture_output=True
)
.stdout.decode("utf-8")
.strip()
.split(":")[1]
)


def release_github() -> Optional[str]:
Expand Down Expand Up @@ -72,9 +74,12 @@ def release_milestone() -> str:
sys.exit(1)

return print_semver(
sorted(v for v in tuple(
parse_semver(cast(str, ms["title"])) for ms in milestones)
if v)[0])
sorted(
v
for v in tuple(parse_semver(cast(str, ms["title"])) for ms in milestones)
if v
)[0]
)


def release_pr_milestone() -> Optional[str]:
Expand All @@ -98,14 +103,18 @@ def release_local(path: str) -> tuple[Optional[str], str, bool]:
with open(os.path.join(path, "BUILD.bazel"), "r") as fh:
bzl = ast.parse(fh.read(), filename=path)
for stmt in bzl.body:
if (isinstance(stmt, ast.Expr)
and isinstance(stmt.value, ast.Call)
and isinstance(stmt.value.func, ast.Name)
and stmt.value.func.id == "haskell_library"):
if (
isinstance(stmt, ast.Expr)
and isinstance(stmt.value, ast.Call)
and isinstance(stmt.value.func, ast.Name)
and stmt.value.func.id == "haskell_library"
):
for arg in stmt.value.keywords:
if (arg.arg == "version"
and isinstance(arg.value, ast.Constant)
and isinstance(arg.value.s, str)):
if (
arg.arg == "version"
and isinstance(arg.value, ast.Constant)
and isinstance(arg.value.s, str)
):
return arg.value.s, "BUILD.bazel", True

if os.path.exists(os.path.join(path, "configure.ac")):
Expand Down Expand Up @@ -147,24 +156,34 @@ def main(prog: str, args: List[str]) -> None:
print(f"Local release: {local_release} ({local_origin})")

if local_required and gh_release and gh_release != local_release:
print(f"\nFAIL: GitHub draft release {gh_release} does not match "
f"{local_origin} {local_release}")
print(
f"\nFAIL: GitHub draft release {gh_release} does not match "
f"{local_origin} {local_release}"
)
sys.exit(1)
if local_required and ms_release != local_release:
print(f"\nFAIL: Next GitHub Milestone release {ms_release} does not "
f"match {local_origin} {local_release}")
print(
f"\nFAIL: Next GitHub Milestone release {ms_release} does not "
f"match {local_origin} {local_release}"
)
sys.exit(1)
if local_required and pr_release and pr_release != local_release:
print(f"\nFAIL: PR milestone {pr_release} does not match "
f"{local_origin} {local_release}")
print(
f"\nFAIL: PR milestone {pr_release} does not match "
f"{local_origin} {local_release}"
)
sys.exit(1)
if gh_release and gh_release != ms_release:
print(f"\nFAIL: GitHub draft release {gh_release} does not match "
f"next GitHub Milestone release {ms_release}")
print(
f"\nFAIL: GitHub draft release {gh_release} does not match "
f"next GitHub Milestone release {ms_release}"
)
sys.exit(1)
if pr_release and pr_release != ms_release:
print(f"\nFAIL: PR milestone {pr_release} does not match "
f"next GitHub Milestone release {ms_release}")
print(
f"\nFAIL: PR milestone {pr_release} does not match "
f"next GitHub Milestone release {ms_release}"
)
sys.exit(1)

print(f"\nPASS: Upcoming release version is {gh_release}")
Expand Down
51 changes: 42 additions & 9 deletions platform/appimage/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
# Fail out on error
set -exuo pipefail

# https://stackoverflow.com/questions/72978485/git-submodule-update-failed-with-fatal-detected-dubious-ownership-in-reposit
git config --global --add safe.directory '*'

# Ensure consistent file permissions
umask 022

# Support reproducible builds
if [ -z "${SOURCE_DATE_EPOCH:-}" ]; then
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
fi

usage() {
echo "$0 --src-dir SRC_DIR --project-name PROJECT_NAME [cmake args]"
echo "Builds an app image in the CWD based off PROJECT_NAME installation at SRC_DIR"
Expand Down Expand Up @@ -60,24 +71,31 @@ if [ -z "${PROJECT_NAME+x}" ]; then
exit 1
fi

# https://stackoverflow.com/questions/72978485/git-submodule-update-failed-with-fatal-detected-dubious-ownership-in-reposit
git config --global --add safe.directory '*'

# Check if we can git describe
git describe --tags --match 'v*'

# directory paths
readonly BUILD_DIR="$(realpath .)"
readonly PROJECT_APP_DIR="$BUILD_DIR/$PROJECT_NAME.AppDir"

# Pin appimagetool version for reproducibility
readonly APPIMAGE_TOOL_VERSION="940"
readonly APPIMAGE_TOOL_URL="https://github.com/probonopd/go-appimage/releases/download/continuous/appimagetool-$APPIMAGE_TOOL_VERSION-x86_64.AppImage"

rm -f appimagetool-*.AppImage
wget "https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases/expanded_assets/continuous -O - | grep "appimagetool-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2)"
wget "$APPIMAGE_TOOL_URL" -O "appimagetool-$APPIMAGE_TOOL_VERSION-x86_64.AppImage"
chmod +x appimagetool-*.AppImage

# Extract tool to a isolated directory to avoid polluting the workspace/AppImage
readonly TOOL_EXTRACT_DIR="$BUILD_DIR/tool-extract"
rm -rf "$TOOL_EXTRACT_DIR"
"./appimagetool-$APPIMAGE_TOOL_VERSION-x86_64.AppImage" --appimage-extract
mv squashfs-root "$TOOL_EXTRACT_DIR"

# Patch the tool for musl compatibility.
# https://github.com/probonopd/go-appimage/blob/fced8b8831039daa246ab355f4e2335074abc206/src/appimagetool/appdirtool.go#L400
# This line in the appimagetool breaks musl DNS lookups (looking for /EEE/resolv.conf).
./appimagetool-*.AppImage --appimage-extract
sed -i -e 's!/EEE!/etc!g' squashfs-root/usr/bin/appimagetool
sed -i -e 's!/EEE!/etc!g' "$TOOL_EXTRACT_DIR/usr/bin/appimagetool"

export PKG_CONFIG_PATH=/opt/buildhome/lib/pkgconfig

Expand All @@ -92,13 +110,21 @@ cmake "$SRC_DIR" \
-DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_EXE_LINKER_FLAGS="-Wl,--build-id=none" \
-B _build \
"$@"
cmake --build _build
cmake --install _build --prefix "$PROJECT_NAME.AppDir/usr"

ccache --show-stats

# Normalize file permissions and timestamps for reproducibility
echo "Normalizing AppDir..."
find "$PROJECT_APP_DIR" -exec touch -h -d @"$SOURCE_DATE_EPOCH" {} +
find "$PROJECT_APP_DIR" -type d -exec chmod 0755 {} +
find "$PROJECT_APP_DIR" -type f -perm /0111 -exec chmod 0755 {} +
find "$PROJECT_APP_DIR" -type f ! -perm /0111 -exec chmod 0644 {} +

export QTDIR=/opt/buildhome/qt
export LD_LIBRARY_PATH="/opt/buildhome/lib:/opt/buildhome/lib64:$QTDIR/lib"

Expand All @@ -109,12 +135,19 @@ cp -r "$QTDIR/plugins/platforms/libqwayland-generic.so" "$PROJECT_APP_DIR/$QTDIR
# Copy the tls plugins to the app dir, needed for https connections.
cp -r "$QTDIR/plugins/tls/" "$PROJECT_APP_DIR/$QTDIR/plugins/"

squashfs-root/AppRun -s deploy "$PROJECT_APP_DIR"/usr/share/applications/*.desktop
"$TOOL_EXTRACT_DIR/AppRun" -s deploy "$PROJECT_APP_DIR"/usr/share/applications/*.desktop

# print all links not contained inside the AppDir
LD_LIBRARY_PATH='' find "$PROJECT_APP_DIR" -type f -exec ldd {} \; 2>&1 | grep '=>' | grep -v "$PROJECT_APP_DIR"

squashfs-root/AppRun "$PROJECT_APP_DIR"
# appimagetool (go-appimage) passes -fstime to mksquashfs, which conflicts with
# SOURCE_DATE_EPOCH environment variable.
(
unset SOURCE_DATE_EPOCH
"$TOOL_EXTRACT_DIR/AppRun" "$PROJECT_APP_DIR"
)

APPIMAGE_FILE="$PROJECT_NAME-$(git rev-parse --short HEAD | head -c7)-$ARCH.AppImage"
# Deterministic filename
readonly SHA="$(git rev-parse --short HEAD | head -c7)"
APPIMAGE_FILE="$PROJECT_NAME-$SHA-$ARCH.AppImage"
sha256sum "$APPIMAGE_FILE" >"$APPIMAGE_FILE.sha256"
50 changes: 50 additions & 0 deletions tools/verify_all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright © 2026 The TokTok team
import argparse
import sys
from dataclasses import dataclass

import verify_appimage
import verify_common
from lib import git


@dataclass
class Config:
tag: str
repo: str


def parse_args() -> Config:
parser = argparse.ArgumentParser(
description="Run all reproducibility verifications."
)
parser.add_argument("--tag", help="Tag to verify", default=git.current_tag())
parser.add_argument(
"--repo", help="Repository name", default=verify_common.get_default_repo()
)
return Config(**vars(parser.parse_args()))


def main(config: Config) -> int:
tag = config.tag
repo = config.repo

failed = []

print(f"--- Running verify_appimage for {repo} {tag} ---", file=sys.stderr)
appimage_config = verify_appimage.Config(tag=tag, repo=repo)
if verify_appimage.main(appimage_config) != 0:
failed.append("verify_appimage.py")

if failed:
print(f"Verifications failed: {', '.join(failed)}", file=sys.stderr)
return 1

print(f"All verifications passed for {repo} {tag}!", file=sys.stderr)
return 0


if __name__ == "__main__":
sys.exit(main(parse_args()))
Loading
Loading