diff --git a/Makefile b/Makefile index c835c34127..0c9a69fc3b 100644 --- a/Makefile +++ b/Makefile @@ -429,6 +429,36 @@ refresh-lock-files: scan-image-vulnerabilities: python ci/security-scan/quay_security_analysis.py +ARCH := $(shell uname -m) +ifeq ($(ARCH),amd64) + ARCH := x86_64 +else ifeq ($(ARCH),arm64) + ARCH := aarch64 +endif + +ZIG_VERSION := 0.15.2 +ZIG_BINARY := zig-$(ZIG_VERSION) + +bin/zig-$(ZIG_VERSION): + @echo "Installing Zig $(ZIG_VERSION)..." + TMPDIR=$(shell mktemp -d) + wget --progress=dot:giga https://ziglang.org/download/$(ZIG_VERSION)/zig-$(ARCH)-linux-$(ZIG_VERSION).tar.xz + tar -xJf zig-$(ARCH)-linux-$(ZIG_VERSION).tar.xz -C $$TMPDIR --strip-components=1 + rm -rf zig-$(ARCH)-linux-$(ZIG_VERSION).tar.xz + + mv $$TMPDIR bin/zig-$(ZIG_VERSION) + @echo "Zig installed as bin/zig-$(ZIG_VERSION)" + +# This should be .PHONY because it's an alias/action +.PHONY: install-zig +install-zig: bin/zig-$(ZIG_VERSION) + @echo "Zig is ready to use!" + +# Another .PHONY target for cleanup +.PHONY: clean-zig +clean-zig: + rm -rf bin/zig-$(ZIG_VERSION) + # This is used primarily for gen_gha_matrix_jobs.py to we know the set of all possible images we may want to build .PHONY: all-images ifeq ($(RELEASE_PYTHON_VERSION), 3.12) diff --git a/runtimes/minimal/ubi9-python-3.12/Dockerfile.cpu b/runtimes/minimal/ubi9-python-3.12/Dockerfile.cpu index 4a16d6f7b9..460d3c21ef 100644 --- a/runtimes/minimal/ubi9-python-3.12/Dockerfile.cpu +++ b/runtimes/minimal/ubi9-python-3.12/Dockerfile.cpu @@ -13,6 +13,8 @@ WORKDIR /opt/app-root/bin # OS Packages needs to be installed as root USER 0 +RUN ls /mnt && true + ### BEGIN upgrade first to avoid fixable vulnerabilities # If we have a Red Hat subscription prepared, refresh it RUN /bin/bash <<'EOF' @@ -39,7 +41,7 @@ ARCH=$(uname -m) echo "Detected architecture: $ARCH" PACKAGES="perl mesa-libGL skopeo" if [ "$ARCH" = "s390x" ] || [ "$ARCH" = "ppc64le" ]; then - PACKAGES="$PACKAGES gcc g++ make openssl-devel autoconf automake libtool cmake" + PACKAGES="$PACKAGES gcc g++ ninja-build openssl-devel autoconf automake libtool cmake" fi dnf install -y --setopt=keepcache=1 $PACKAGES EOF diff --git a/scripts/sandbox.py b/scripts/sandbox.py index a8592b09d9..6574b74f7e 100755 --- a/scripts/sandbox.py +++ b/scripts/sandbox.py @@ -47,7 +47,39 @@ def main() -> int: with tempfile.TemporaryDirectory(delete=True) as tmpdir: setup_sandbox(prereqs, pathlib.Path(tmpdir)) - command = [arg if arg != "{};" else tmpdir for arg in args.remaining[1:]] + additional_arguments = [ + # Mount the zigcc utility + f"--volume={os.getcwd()}/bin/zig-0.15.2:/mnt", + f"--env=ZIGCC_ARCH={args.platform.split('/')[1]}", + "--unsetenv=ZIGCC_ARCH", + # CMake heeds these + "--env=CC=/mnt/cc", + "--env=CXX=/mnt/c++", + "--unsetenv=CC", + "--unsetenv=CXX", + # CMake ignores these + "--env=AR=/mnt/ar", + "--env=RANLIB=/mnt/ranlib", + "--env=STRIP=/mnt/strip", + "--unsetenv=AR", + "--unsetenv=RANLIB", + "--unsetenv=STRIP", + # Workaround for a s390x compilation issue + # Codeserver: SPDLOG_CONSTEXPR_FUNC is to work around a consteval issue with zig c++ + # ../deps/spdlog/include/spdlog/details/fmt_helper.h:105:54: error: call to consteval function 'fmt::basic_format_string<...>' is not a constant expression + # Clang (via Zig) is stricter about consteval requirements than GCC + # The format string "{:02}" cannot be evaluated as a constant expression in this context + "--env=CXXFLAGS=-Dundefined=64 -DFMT_CONSTEVAL= -DSPDLOG_CONSTEXPR_FUNC=", + "--unsetenv=CXXFLAGS", + + tmpdir, + ] + command = [] + for arg in args.remaining[1:]: + if arg == "{};": + command.extend(additional_arguments) + else: + command.append(arg) print(f"running {command=}") try: subprocess.check_call(command) @@ -56,6 +88,64 @@ def main() -> int: return err.returncode return 0 +""" +Downloading jedi + × Failed to build `pyzmq==27.1.0` + ├─▶ The build backend returned an error + ╰─▶ Call to `scikit_build_core.build.build_wheel` failed (exit status: 1) + [stdout] + *** scikit-build-core 0.11.6 using CMake 3.26.5 (wheel) + *** Configuring CMake... + loading initial cache file /tmp/tmpf9bnfh5o/build/CMakeInit.txt + -- Configuring incomplete, errors occurred! + [stderr] + CMake Error at /usr/share/cmake/Modules/CMakeDetermineCCompiler.cmake:49 + (message): + Could not find compiler set in environment variable CC: + /mnt/zig-0.15.1/zig cc -target s390x-linux-gnu. + Call Stack (most recent call first): + CMakeLists.txt:2 (project) +""" + +""" +creating build/temp.linux-s390x-cpython-312/psutil/arch/linux + /mnt/zig cc -target s390x-linux-gnu -fno-strict-overflow + -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG + -O2 -fexceptions -g -grecord-gcc-switches -pipe + -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 + -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong + -m64 -march=z14 -mtune=z15 -fasynchronous-unwind-tables + -fstack-clash-protection -O2 -fexceptions -g -grecord-gcc-switches + -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 + -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong + -m64 -march=z14 -mtune=z15 -fasynchronous-unwind-tables + -fstack-clash-protection -O2 -fexceptions -g -grecord-gcc-switches + -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 + -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong + -m64 -march=z14 -mtune=z15 -fasynchronous-unwind-tables + -fstack-clash-protection -fPIC -DPSUTIL_POSIX=1 -DPSUTIL_SIZEOF_PID_T=4 + -DPSUTIL_VERSION=700 -DPy_LIMITED_API=0x03060000 + -DPSUTIL_LINUX=1 -I/tmp/.tmpWlL4ZP/builds-v0/.tmpOwAhw2/include + -I/usr/include/python3.12 -c psutil/_psutil_common.c -o + build/temp.linux-s390x-cpython-312/psutil/_psutil_common.o + [stderr] + /tmp/.tmpWlL4ZP/builds-v0/.tmpOwAhw2/lib64/python3.12/site-packages/setuptools/dist.py:759: + SetuptoolsDeprecationWarning: License classifiers are deprecated. + !! + + ******************************************************************************** + Please consider removing the following classifiers in favor of a + SPDX license expression: + License :: OSI Approved :: BSD License + See + https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license + for details. + + ******************************************************************************** + !! + self._finalize_license_expression() + error: unsupported preprocessor arg: -D_FORTIFY_SOURCE +""" def buildinputs( dockerfile: pathlib.Path | str, @@ -63,6 +153,12 @@ def buildinputs( ) -> list[pathlib.Path]: if not (ROOT_DIR / "bin/buildinputs").exists(): subprocess.check_call([MAKE, "bin/buildinputs"], cwd=ROOT_DIR) + if not (ROOT_DIR / "bin/zig-0.15.2").exists(): + subprocess.check_call([MAKE, "bin/zig-0.15.2"], cwd=ROOT_DIR) + if not (ROOT_DIR / "bin/zig-0.15.2/zigcc").exists(): + subprocess.check_call([MAKE, "build"], cwd=ROOT_DIR / "scripts/zigcc") + for alias in ["cc", "c++", "ar", "llvm-ar", "ranlib", "llvm-ranlib", "strip", "llvm-strip"]: + shutil.copy(ROOT_DIR / "scripts/zigcc/bin/zigcc", ROOT_DIR / "bin/zig-0.15.2" / alias) stdout = subprocess.check_output([ROOT_DIR / "bin/buildinputs", str(dockerfile)], text=True, cwd=ROOT_DIR, env={"TARGETPLATFORM": platform, **os.environ}) diff --git a/scripts/zigcc/Makefile b/scripts/zigcc/Makefile new file mode 100644 index 0000000000..23ff791e8a --- /dev/null +++ b/scripts/zigcc/Makefile @@ -0,0 +1,21 @@ +.PHONY: build test clean + +build: bin/zigcc + +# always build for linux and the machine's native architecture +bin/zigcc: *.go go.mod + GOOS=linux go build -o $@ -ldflags="-s -w" -v ./... + +test: + go test -v ./... + +fmt: + go fmt ./... + +vet: + go vet ./... + +clean: + go clean + rm -f bin/* + rmdir bin diff --git a/scripts/zigcc/README.md b/scripts/zigcc/README.md new file mode 100644 index 0000000000..b51b279e36 --- /dev/null +++ b/scripts/zigcc/README.md @@ -0,0 +1,68 @@ +# zigcc + +_Launcher for `zig cc`, `zig c++`, and related subcommands for more efficient cross-compilation_ + +This script helps by using a native compiler binary to compile for the target architecture. +This means that the compiler then is running at the native speed. + +## Background + +Cross-compilation means compiling for a different architecture than the one on which the compiler is running. +Specifically, it is when you're creating a workbench container image for say x86_64 on an arm64 machine such as an M-series MacBook. +Everything that's running in the container will run slower, because it has to run under qemu. + +This slowdown is especially noticeable when compiling C/C++ code for IBM Power and Z, such as Python extension modules that don't have precompiled binaries for these architectures on PyPI. + +## Usage + +```commandline +gmake codeserver-ubi9-python-3.12 BUILD_ARCH=linux/s390x CONTAINER_BUILD_CACHE_ARGS= +``` + +This is about 50% faster than cross-compiling through `qemu-s390x-static` or `qemu-ppc64le-static`. + +## Cross-compilation overview + +### Qemu-user-static + +Docker/Podman can perform cross-compilation using `qemu-user-static`. +The idea is to install the various `qemu-user` binaries as interpreters for foreign architecture binaries. +Launching such binary will then automatically run it under qemu interpreter. + +Docker is uniquely suitable to run binaries like this, because container images bring all dependencies with them. + +### Traditional cross-compilation + +For CMake, I can imagine an approach which involves installing a cross compiler and mounting arm64 docker image to provide arm64 environment with libraries. + + +### Zig + +https://zig.guide/working-with-c/zig-cc/ + +The `zig cc` command bundles clang in a way that simplifies its usage for cross compilation, + + +#### Wrapper (zigcc.go) + +We need a wrapper so that we can transform CLI arguments to work with `zig cc`. + +The main problem is the `-Wl,D_FORTIFY_SOURCE=2` flag, because zig has limited handling for -Wl, and does not do -Wl,D correctly. + +The wrapper should be written in a low-overhead language, like Go, or possibly Bash, certainly not Python. +The lower the overhead of the wrapper, the better, since the compiler is invoked many times during a typical build. + +### Debugging + +To observe the effect of the wrapper, we can use `execsnoop` from `bcc-tools` to monitor the compiler invocations during a container build. + +```commandline +$ podman machine ssh +# bootc usr-overlay +# dnf install bcc-tools +# /usr/share/bcc/tools/execsnoop +``` + +## Credits + +This is inspired by diff --git a/scripts/zigcc/go.mod b/scripts/zigcc/go.mod new file mode 100644 index 0000000000..e2b750b7bb --- /dev/null +++ b/scripts/zigcc/go.mod @@ -0,0 +1,3 @@ +module zigcc + +go 1.24 diff --git a/scripts/zigcc/test/Dockerfile b/scripts/zigcc/test/Dockerfile new file mode 100644 index 0000000000..ed8b52f261 --- /dev/null +++ b/scripts/zigcc/test/Dockerfile @@ -0,0 +1,35 @@ +FROM quay.io/centos/centos:stream9 + +# Don't install the GCC compiler, we will inject our own CC +#RUN dnf install -y gcc + +# Check CC is in fact injected +RUN /bin/bash <<'EOF' +set -Eeuxo pipefail +ls ${CC} +${CC} --version +EOF + +#RUN /mnt/zig clang -x c /dev/null --no-default-config -target s390x-unknown-linux5.10.0-gnu2.34.0 -mhard-float -fno-PIE -fPIC -gdwarf-4 -gdwarf32 -fno-lto -MD -MV -MF /root/.cache/zig/tmp/4ee4407ea328f5c8-null.o.d -fhosted -fno-omit-frame-pointer +#RUN /mnt/zig cc -target s390x-linux-gnu.2.34 --sysroot / -isystem /usr/include -L/usr/lib64 -isystem /usr/local/include -L/usr/local/lib64 -dM -E -x c /dev/null +RUN /mnt/zig -cc1 -triple x86_64-unknown-linux5.10.0-gnu2.34.0 -E -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name abi-note.S -mrelocation-model pic -pic-level 2 -fhalf-no-semantic-interposition -mframe-pointer=all -fmath-errno -ffp-contract=on -fno-rounding-math + +# Create a directory for the program +RUN mkdir /app + +# Copy the C source code into the container +RUN sh -c 'cat > /app/hello.c' < +int main() { + printf("Hello, World!\\n"); + return 0; +} +EOF + +# Compile the C program +RUN ${CC} /app/hello.c -o /app/hello +# Check it works +RUN /app/hello + +# Set the entry point to run the compiled program +CMD ["/app/hello"] diff --git a/scripts/zigcc/test/run.py b/scripts/zigcc/test/run.py new file mode 100644 index 0000000000..a850d3e68e --- /dev/null +++ b/scripts/zigcc/test/run.py @@ -0,0 +1,46 @@ +#! /usr/bin/env python3 + +import argparse +import logging +import pathlib +import subprocess +import sys +from typing import cast + +ROOT_DIR = pathlib.Path(__file__).parent.parent.parent.parent + +logging.basicConfig(level=logging.INFO) +logging.root.name = pathlib.Path(__file__).name + + +class Args(argparse.Namespace): + platform: str + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--platform", default="linux/amd64", help="Target platform for the build") + args = cast(Args, p.parse_args()) + + dockerfile_path = ROOT_DIR / "scripts" / "zigcc" / "test" / "Dockerfile" + sandbox_script_path = ROOT_DIR / "scripts" / "sandbox.py" + + return subprocess.call( + [sys.executable, str(sandbox_script_path), + "--dockerfile", str(dockerfile_path), + "--platform", args.platform, + "--", + "podman", "build", + "--no-cache", + "--platform", args.platform, + "-t", "hello-world-test", + # dockerfile path in podman command is required, Dockerfile is not copied to sandbox + "-f", str(dockerfile_path), + "{};"], + # sandbox.py assumes running from repo root + cwd=ROOT_DIR + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/zigcc/zigcc.go b/scripts/zigcc/zigcc.go new file mode 100644 index 0000000000..4e76ebbe81 --- /dev/null +++ b/scripts/zigcc/zigcc.go @@ -0,0 +1,125 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "syscall" +) + +const ( + zig = "/mnt/zig" + glibcVersion = "2.34" +) + +func getTarget(subcommand string, args []string) string { + var arch string + switch os.Getenv("ZIGCC_ARCH") { + case "amd64": + arch = "x86_64" + case "arm64": + arch = "aarch64" + case "ppc64le": + arch = "powerpc64le" + case "s390x": + arch = "s390x" + default: + fmt.Fprintf(os.Stderr, "zigcc.go: Error: unknown architecture: %s\n", os.Getenv("ZIGCC_ARCH")) + os.Exit(1) + } + + // target glibc 2.28 or newer (supports FORTIFY_SOURCE) + //return arch + "-linux-gnu" + // specify glibc version with . separator between gnu and the version, otherwise + // error: unable to parse target query 'x86_64-linux-gnu2.34': UnknownApplicationBinaryInterface + // error: unable to parse target query 'x86_64-linux-gnu+2.34': UnknownApplicationBinaryInterface + // error: unable to parse target query 'x86_64-unknown-linux-gnu.2.34': UnknownOperatingSystem + // error: unable to parse target query 'x86_64-linux-gnuabi2.34': UnknownApplicationBinaryInterface + // but for some reason, I'm still having problems with npm + // zig c++ -target s390x-linux-gnu.2.34 -o Release/obj.target/windows.node -shared -pthread -rdynamic -m64 -march=z196 -Wl,-soname=windows.node -Wl,--start-group -Wl,--end-group + // npm error zig: error: version '.2.34' in target triple 's390x-unknown-linux-gnu.2.34' is invalid + if subcommand == "c++" { + // https://github.com/ziglang/zig/issues/25994#issuecomment-3562961055 + return arch + "-linux-gnu" + } + if slices.Contains(args, "--version") || slices.Contains(args, "-v") { + // https://github.com/ziglang/zig/issues/22269 + return arch + "-linux-gnu" + } + return arch + "-linux-gnu" + "." + glibcVersion +} + +func processArg0(arg0 string) (string, error) { + switch arg0 { + case "cc": + return "cc", nil + case "c++": + return "c++", nil + + // `llvm-` prefix so that CMake finds it + // https://gitlab.kitware.com/cmake/cmake/-/issues/23554 + // https://gitlab.kitware.com/cmake/cmake/-/issues/18712#note_1006035 + // ../../libtool: line 1887: /mnt/ar: No such file or directory + case "ar", "llvm-ar": + return "ar", nil + case "ranlib", "llvm-ranlib": + return "ranlib", nil + case "strip", "llvm-strip": + return "strip", nil + + default: + return "", fmt.Errorf("zigcc.go: Error: unknown wrapper name: %s", arg0) + } +} + +func processArgs(args []string) []string { + newArgs := make([]string, 0, len(args)) + for _, arg := range args { + // deal with -Wp,-D_FORTIFY_SOURCE=2: + // this comes in https://github.com/giampaolo/psutil/blob/master/setup.py#L254 + // build defaults to using python's flags and they are the RHEL fortified ones + if strings.HasPrefix(arg, "-Wp,") { + newArgs = append(newArgs, strings.Split(arg, ",")[1:]...) + } else { + newArgs = append(newArgs, arg) + } + } + return newArgs +} + +func main() { + arg0 := filepath.Base(os.Args[0]) + subcommand, err := processArg0(arg0) + if err != nil { + fmt.Fprintf(os.Stderr, "zigcc.go: Error: %v\n", err) + os.Exit(1) + } + + argv := os.Args[1:] + target := getTarget(subcommand, argv) + + newArgs := []string{ + zig, + subcommand, + } + if subcommand == "cc" || subcommand == "c++" { + newArgs = append(newArgs, "-target", target) + // codeserver: :33:10: fatal error: 'X11/Xlib.h' file not found + // -isystem=... does not work, requires passing as two separate args + newArgs = append(newArgs, + "--sysroot", "/", + "-isystem", "/usr/include", + "-L/usr/lib64", + "-isystem", "/usr/local/include", + "-L/usr/local/lib64") + } + newArgs = append(newArgs, processArgs(argv)...) + + env := os.Environ() + if err := syscall.Exec(newArgs[0], newArgs, env); err != nil { + fmt.Fprintf(os.Stderr, "zigcc.go: Error executing zig: %v\n", err) + os.Exit(1) + } +} diff --git a/scripts/zigcc/zigcc_test.go b/scripts/zigcc/zigcc_test.go new file mode 100644 index 0000000000..1bea00d6e0 --- /dev/null +++ b/scripts/zigcc/zigcc_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "reflect" + "testing" +) + +func TestProcessWp(t *testing.T) { + args := []string{"-Wp,-D_FORTIFY_SOURCE=2"} + newArgs := processArgs(args) + if !reflect.DeepEqual(newArgs, []string{"-D_FORTIFY_SOURCE=2"}) { + t.Fatalf("expected -DFOO=bar, got %v", newArgs) + } + for _, tc := range []struct { + args []string + expected []string + }{ + { + args: []string{"-Wp,-D_FORTIFY_SOURCE=2"}, + expected: []string{"-D_FORTIFY_SOURCE=2"}, + }, + { + args: []string{"-Wp,-DNDEBUG,-D_FORTIFY_SOURCE=2"}, + expected: []string{"-DNDEBUG", "-D_FORTIFY_SOURCE=2"}, + }, + } { + t.Run(fmt.Sprint(tc.args), func(t *testing.T) { + newArgs := processArgs(tc.args) + if !reflect.DeepEqual(newArgs, tc.expected) { + t.Fatalf("expected %#v, got %#v", tc.expected, newArgs) + } + }) + } +}