diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f2fbf8a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: test + +on: + push: + branches: [master] + pull_request: + +jobs: + test: + name: ./test.sh + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + arch: x86_64 + - os: ubuntu-latest + arch: aarch64 + - os: macos-latest + arch: arm64 + runs-on: ${{ matrix.arch == 'aarch64' && 'ubuntu-24.04-arm' || matrix.os }} + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.14" + - run: ./test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..120de9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.git/ +repo/ + +# build artifacts (applies to all packages) +build/ +dist/ +*.egg-info/ + +# downloaded toolchain binaries +gcc-arm-none-eabi/gcc_arm_none_eabi/toolchain/ diff --git a/README.md b/README.md index 049b91a..42a8b4f 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ # dependencies + +a central repo for managing and vendoring third party dependencies for all comma projects. + +all of our projects are python projects, so each of these gets packaged as a pip project. + +this vendoring serves a few goals: +- all dependencies are centrally managed here +- we can slim them down to only what we need +- all platforms get the same versions installed +- tighter control of distribution for fast installs (e.g. Ubuntu's `apt-get` is slow) + +we target the following platforms: +- Linux x86_64 (glibc) +- Linux aarch64 (glibc) +- Darwin aarch64 (Apple Silicon) + +contributions welcome for other platforms! diff --git a/gcc-arm-none-eabi/build.sh b/gcc-arm-none-eabi/build.sh new file mode 100755 index 0000000..8c984a8 --- /dev/null +++ b/gcc-arm-none-eabi/build.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$DIR" + +TOOLCHAIN_VERSION="13.2.rel1" +TOOLCHAIN_BASE="arm-gnu-toolchain-${TOOLCHAIN_VERSION}" +GCC_VERSION="13.2.1" + +INSTALL_DIR="$DIR/gcc_arm_none_eabi/toolchain" + +# Idempotent: skip if already built +if [ -x "$INSTALL_DIR/bin/arm-none-eabi-gcc" ]; then + echo "Toolchain already present, skipping download." + exit 0 +fi + +# Detect current platform +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "${OS}-${ARCH}" in + Linux-x86_64) PLATFORM_SUFFIX="x86_64" ;; + Linux-aarch64) PLATFORM_SUFFIX="aarch64" ;; + Darwin-arm64) PLATFORM_SUFFIX="darwin-arm64" ;; + *) + echo "Unsupported platform: ${OS}-${ARCH}" >&2 + exit 1 + ;; +esac + +TARBALL="${TOOLCHAIN_BASE}-${PLATFORM_SUFFIX}-arm-none-eabi.tar.xz" +URL="https://developer.arm.com/-/media/Files/downloads/gnu/${TOOLCHAIN_VERSION}/binrel/${TARBALL}" + +# Download +echo "Downloading $TARBALL ..." +curl -fSL -o "$TARBALL" "$URL" + +# Extract +echo "Extracting ..." +tar xf "$TARBALL" +EXTRACT_DIR=$(ls -d arm-gnu-toolchain-*-${PLATFORM_SUFFIX}-arm-none-eabi) + +SRC="$DIR/$EXTRACT_DIR" + +rm -rf "$INSTALL_DIR" +mkdir -p "$INSTALL_DIR" + +# --- bin: only the tools directly used by SConscript --- +mkdir -p "$INSTALL_DIR/bin" +for tool in gcc objcopy size; do + if [ -f "$SRC/bin/arm-none-eabi-$tool" ]; then + cp "$SRC/bin/arm-none-eabi-$tool" "$INSTALL_DIR/bin/" + fi +done + +# --- libexec: cc1 and collect2 (needed by gcc driver) --- +LIBEXEC_SRC="$SRC/libexec/gcc/arm-none-eabi/$GCC_VERSION" +LIBEXEC_DST="$INSTALL_DIR/libexec/gcc/arm-none-eabi/$GCC_VERSION" +mkdir -p "$LIBEXEC_DST" +for f in cc1 collect2 liblto_plugin.so liblto_plugin.0.so; do + if [ -f "$LIBEXEC_SRC/$f" ]; then + cp "$LIBEXEC_SRC/$f" "$LIBEXEC_DST/" + fi +done + +# --- arm-none-eabi/bin: only tools gcc calls internally --- +ARM_SRC="$SRC/arm-none-eabi" +ARM_DST="$INSTALL_DIR/arm-none-eabi" +mkdir -p "$ARM_DST/bin" +for tool in as ld ld.bfd; do + if [ -f "$ARM_SRC/bin/$tool" ]; then + cp "$ARM_SRC/bin/$tool" "$ARM_DST/bin/" + fi +done + +# --- newlib C headers (needed for #include_next from GCC's stdint.h etc.) --- +mkdir -p "$ARM_DST/include" +find "$ARM_SRC/include" -maxdepth 1 -not -name 'c++' | while read -r item; do + [ "$item" = "$ARM_SRC/include" ] && continue + cp -r "$item" "$ARM_DST/include/" +done + +# --- lib/gcc: compiler support (only the multilib we use) --- +LIB_GCC_SRC="$SRC/lib/gcc/arm-none-eabi/$GCC_VERSION" +LIB_GCC_DST="$INSTALL_DIR/lib/gcc/arm-none-eabi/$GCC_VERSION" +MULTILIB="thumb/v7e-m+dp/hard" +mkdir -p "$LIB_GCC_DST/$MULTILIB" + +# compiler-provided headers +cp -r "$LIB_GCC_SRC/include" "$LIB_GCC_DST/" +if [ -d "$LIB_GCC_SRC/include-fixed" ]; then + cp -r "$LIB_GCC_SRC/include-fixed" "$LIB_GCC_DST/" +fi + +# target multilib: only thumb/v7e-m+dp/hard (cortex-m7 hard-float) +cp "$LIB_GCC_SRC/$MULTILIB"/libgcc.a "$LIB_GCC_DST/$MULTILIB/" +cp "$LIB_GCC_SRC/$MULTILIB"/crt*.o "$LIB_GCC_DST/$MULTILIB/" 2>/dev/null || true + +# --- remove unused headers --- +rm -f "$LIB_GCC_DST/include/arm_neon.h" +rm -f "$LIB_GCC_DST/include/arm_mve_types.h" +rm -f "$LIB_GCC_DST/include/mmintrin.h" +rm -f "$LIB_GCC_DST/include/ISO_Fortran_binding.h" +rm -f "$LIB_GCC_DST/include/gcov.h" +rm -f "$LIB_GCC_DST/include/arm_cde.h" + +# --- strip host binaries --- +find "$INSTALL_DIR" -type f \( -executable -o -name '*.so' \) -exec strip {} + 2>/dev/null || true + +# --- clean up download artifacts --- +rm -rf "$EXTRACT_DIR" +rm -f "$TARBALL" + +echo "Installed to $INSTALL_DIR" +du -sh "$INSTALL_DIR" diff --git a/gcc-arm-none-eabi/gcc_arm_none_eabi/__init__.py b/gcc-arm-none-eabi/gcc_arm_none_eabi/__init__.py new file mode 100644 index 0000000..08a237d --- /dev/null +++ b/gcc-arm-none-eabi/gcc_arm_none_eabi/__init__.py @@ -0,0 +1,21 @@ +import os +import sys + +TOOLCHAIN_DIR = os.path.join(os.path.dirname(__file__), "toolchain") + + +def _run(name): + binary = os.path.join(TOOLCHAIN_DIR, "bin", name) + os.execvp(binary, [binary] + sys.argv[1:]) + + +def _run_gcc(): + _run("arm-none-eabi-gcc") + + +def _run_objcopy(): + _run("arm-none-eabi-objcopy") + + +def _run_size(): + _run("arm-none-eabi-size") diff --git a/gcc-arm-none-eabi/pyproject.toml b/gcc-arm-none-eabi/pyproject.toml new file mode 100644 index 0000000..efd348a --- /dev/null +++ b/gcc-arm-none-eabi/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=64", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "gcc-arm-none-eabi" +version = "13.2.1" +description = "ARM GCC toolchain for bare-metal targets" +requires-python = ">=3.8" + +[project.scripts] +arm-none-eabi-gcc = "gcc_arm_none_eabi:_run_gcc" +arm-none-eabi-objcopy = "gcc_arm_none_eabi:_run_objcopy" +arm-none-eabi-size = "gcc_arm_none_eabi:_run_size" + +[tool.setuptools.packages.find] +include = ["gcc_arm_none_eabi*"] + +[tool.setuptools.package-data] +gcc_arm_none_eabi = ["toolchain/**/*"] diff --git a/gcc-arm-none-eabi/setup.py b/gcc-arm-none-eabi/setup.py new file mode 100644 index 0000000..867fc87 --- /dev/null +++ b/gcc-arm-none-eabi/setup.py @@ -0,0 +1,65 @@ +import os +import platform +import subprocess +import sys + +from setuptools.command.build_py import build_py +from setuptools.dist import Distribution + +try: + from wheel.bdist_wheel import bdist_wheel +except ImportError: + bdist_wheel = None + + +class BuildToolchain(build_py): + """Run build.sh to download and prepare the toolchain before collecting package data.""" + + def run(self): + pkg_dir = os.path.dirname(os.path.abspath(__file__)) + toolchain_marker = os.path.join( + pkg_dir, "gcc_arm_none_eabi", "toolchain", "bin", "arm-none-eabi-gcc" + ) + + if not os.path.exists(toolchain_marker): + build_script = os.path.join(pkg_dir, "build.sh") + subprocess.check_call(["bash", build_script], cwd=pkg_dir) + + super().run() + + +cmdclass = {"build_py": BuildToolchain} + +if bdist_wheel is not None: + + class PlatformWheel(bdist_wheel): + """Produce a platform-specific, Python-version-agnostic wheel.""" + + def finalize_options(self): + super().finalize_options() + self.root_is_pure = False + + def get_tag(self): + system = platform.system() + machine = platform.machine() + + if system == "Linux": + plat = f"linux_{machine}" + elif system == "Darwin": + plat = "macosx_11_0_arm64" + else: + plat = f"{system.lower()}_{machine}" + + return "py3", "none", plat + + cmdclass["bdist_wheel"] = PlatformWheel + + +def setup(): + from setuptools import setup as _setup + + _setup(cmdclass=cmdclass) + + +if __name__ == "__main__": + setup() diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..7eea2c8 --- /dev/null +++ b/test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -e + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" + +# Create a temporary virtualenv +VENV_DIR="$(mktemp -d)" +trap 'rm -rf "$VENV_DIR"' EXIT +python3 -m venv "$VENV_DIR" +source "$VENV_DIR/bin/activate" +pip install --upgrade pip >/dev/null + +# Auto-discover packages: any subdirectory containing a pyproject.toml +PACKAGES=() +for toml in "$REPO_DIR"/*/pyproject.toml; do + [ -f "$toml" ] || continue + PACKAGES+=("$(dirname "$toml")") +done + +if [ ${#PACKAGES[@]} -eq 0 ]; then + echo "No packages found." + exit 1 +fi + +FAILED=() + +for pkg in "${PACKAGES[@]}"; do + name="$(basename "$pkg")" + echo "=========================================" + echo "Testing: $name" + echo "=========================================" + + # Install from git URL with subdirectory (simulates real-world usage) + echo "[$name] pip install ..." + pip install "$pkg" --verbose + + # Verify import works + module="${name//-/_}" + echo "[$name] Verifying import of $module ..." + python -c "import $module; print(f'{$module.__name__} OK')" + + echo "[$name] PASSED" + echo +done + +if [ ${#FAILED[@]} -ne 0 ]; then + echo "FAILED packages: ${FAILED[*]}" + exit 1 +fi + +echo "All ${#PACKAGES[@]} package(s) passed." +echo +echo "Installed sizes:" +for pkg in "${PACKAGES[@]}"; do + name="$(basename "$pkg")" + module="${name//-/_}" + mod_dir="$(python -c "import $module, os; print(os.path.dirname($module.__file__))")" + size="$(du -sh "$mod_dir" | cut -f1)" + echo " $name: $size" +done