From e0af3a96e52071e6daa10de7b552bf61f7275754 Mon Sep 17 00:00:00 2001 From: WLTBAgent Date: Sun, 12 Apr 2026 08:06:09 -0700 Subject: [PATCH 1/2] Fix Gentoo/gcc-15 build: resolve getopt type conflict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modern glibc (2.39+) and gcc-15 declare getopt() with a full ANSI prototype in , which conflicts with our bundled K&R-style 'extern int getopt();' declaration when both getopt.h and are included (via super.h → unistd.h). Fix: On modern systems (Linux, BSD, macOS), skip the bundled getopt declarations entirely and use system getopt. Only compile the bundled implementation on truly ancient systems that lack native support. Changes: - src/getopt.h: Add preprocessor guard — modern systems include + instead of bundled declarations - src/getopt.c: Wrap entire bundled implementation in the same guard so we don't emit duplicate getopt symbols Also adds build infrastructure: - Docker build test suite (13 distros including Gentoo) - GitHub Actions CI workflow (11 distros, runs on every push/PR) Fixes #53 --- .github/workflows/build-test.yml | 90 +++++++++ build-test/.gitignore | 1 + build-test/build-test.sh | 307 +++++++++++++++++++++++++++++++ src/getopt.c | 24 +-- src/getopt.h | 37 +++- 5 files changed, 440 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/build-test.yml create mode 100644 build-test/.gitignore create mode 100755 build-test/build-test.sh diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..d41cb57 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,90 @@ +name: Build Test Suite + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - tag: ubuntu:24.04 + pkg: apt + desc: Ubuntu 24.04 LTS + - tag: ubuntu:22.04 + pkg: apt + desc: Ubuntu 22.04 LTS + - tag: debian:12 + pkg: apt + desc: Debian 12 Bookworm + - tag: fedora:41 + pkg: dnf + desc: Fedora 41 + - tag: fedora:40 + pkg: dnf + desc: Fedora 40 + - tag: rockylinux:9 + pkg: dnf + desc: Rocky Linux 9 + - tag: almalinux:9 + pkg: dnf + desc: AlmaLinux 9 + - tag: alpine:3.21 + pkg: apk + desc: Alpine 3.21 + - tag: archlinux:latest + pkg: pacman + desc: Arch Linux + - tag: opensuse/leap:15.6 + pkg: zypper + desc: openSUSE Leap 15.6 + - tag: gentoo/stage3:latest + pkg: emerge + desc: Gentoo (gcc-14+) + + name: "Build: ${{ matrix.desc }}" + + steps: + - uses: actions/checkout@v4 + + - name: Build in Docker (${{ matrix.desc }}) + run: | + case "${{ matrix.pkg }}" in + apt) + INSTALL="apt-get update && apt-get install -y build-essential autoconf automake" + ;; + dnf) + INSTALL="dnf install -y gcc make autoconf automake" + ;; + apk) + INSTALL="apk add --no-cache build-base autoconf automake musl-dev" + ;; + pacman) + INSTALL="pacman -Syu --noconfirm base-devel autoconf automake" + ;; + zypper) + INSTALL="zypper -n refresh && zypper -n install gcc make autoconf automake" + ;; + emerge) + INSTALL="emerge --sync 2>/dev/null; emerge -q sys-devel/gcc sys-devel/autoconf sys-devel/automake" + ;; + esac + + cat > Dockerfile.test </dev/null; ./configure && make clean && make + RUN cd /build && if [ -f src/sudosh ]; then src/sudosh -h 2>&1 || true; fi + EOF + + docker build -f Dockerfile.test -t sudosh2-test . + + - name: Clean up + if: always() + run: docker rmi sudosh2-test 2>/dev/null || true diff --git a/build-test/.gitignore b/build-test/.gitignore new file mode 100644 index 0000000..fbca225 --- /dev/null +++ b/build-test/.gitignore @@ -0,0 +1 @@ +results/ diff --git a/build-test/build-test.sh b/build-test/build-test.sh new file mode 100755 index 0000000..077b8e8 --- /dev/null +++ b/build-test/build-test.sh @@ -0,0 +1,307 @@ +#!/bin/bash +# +# Docker-based build test suite for sudosh2 +# Tests compilation across multiple popular Linux distributions +# +# Usage: +# ./build-test.sh # Run all tests +# ./build-test.sh --single N # Run only test number N +# ./build-test.sh --clean # Remove all Docker images +# ./build-test.sh --list # List supported distros +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +IMAGE_PREFIX="sudosh2-build-test" + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# Define test matrix: "distro:tag|package_manager|description" +DISTROS=( + "ubuntu:24.04|apt|Ubuntu 24.04 LTS (Noble)" + "ubuntu:22.04|apt|Ubuntu 22.04 LTS (Jammy)" + "debian:12|apt|Debian 12 (Bookworm)" + "debian:11|apt|Debian 11 (Bullseye)" + "fedora:41|dnf|Fedora 41" + "fedora:40|dnf|Fedora 40" + "rockylinux:9|dnf|Rocky Linux 9" + "almalinux:9|dnf|AlmaLinux 9" + "alpine:3.21|apk|Alpine 3.21" + "alpine:3.19|apk|Alpine 3.19" + "archlinux:latest|pacman|Arch Linux (rolling)" + "opensuse/leap:15.6|zypper|openSUSE Leap 15.6" + "gentoo/stage3:latest|emerge|Gentoo (rolling, gcc-14+)" +) + +RESULTS_DIR="$SCRIPT_DIR/results" + +usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --clean Remove all Docker images and results" + echo " --list List supported distributions" + echo " --single N Run only test number N (from --list)" + echo " -h, --help Show this help" + exit 0 +} + +list_distros() { + echo -e "${BOLD}Supported build test distributions:${NC}" + echo "" + local i=1 + for entry in "${DISTROS[@]}"; do + IFS='|' read -r tag pkg desc <<< "$entry" + printf " ${CYAN}%2d${NC} %-28s %s\n" "$i" "$tag" "$desc" + ((i++)) + done + echo "" + echo "Total: ${#DISTROS[@]} distributions" +} + +clean() { + echo -e "${YELLOW}Cleaning up...${NC}" + docker images --filter "reference=${IMAGE_PREFIX}*" -q | while read img; do + docker rmi "$img" -f 2>/dev/null || true + done + rm -rf "$RESULTS_DIR" + echo -e "${GREEN}Clean complete.${NC}" + exit 0 +} + +generate_dockerfile() { + local tag="$1" + local pkg="$2" + + local install_cmd="" + local build_cmd="cd /build && autoreconf -fi 2>/dev/null; ./configure && make clean && make" + + case "$pkg" in + apt) + install_cmd="apt-get update && apt-get install -y build-essential autoconf automake" + ;; + dnf) + install_cmd="dnf install -y gcc make autoconf automake" + ;; + apk) + install_cmd="apk add --no-cache build-base autoconf automake musl-dev" + ;; + pacman) + install_cmd="pacman -Syu --noconfirm base-devel autoconf automake" + ;; + zypper) + install_cmd="zypper -n refresh && zypper -n install gcc make autoconf automake" + ;; + emerge) + install_cmd="emerge --sync 2>/dev/null; emerge -q sys-devel/gcc sys-devel/autoconf sys-devel/automake" + build_cmd="cd /build && ./configure && make clean && make" + ;; + esac + + cat <&1 || true; fi +RUN cd /build && if [ -f src/sudosh-replay ]; then src/sudosh-replay -h 2>&1 || true; fi + +EOF +} + +run_test() { + local index="$1" + local entry="$2" + + IFS='|' read -r tag pkg desc <<< "$entry" + + # Sanitize tag for Docker image name + safe_tag=$(echo "$tag" | tr '/:' '-') + image_name="${IMAGE_PREFIX}-${safe_tag}" + + echo -e "" + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BOLD} Building: ${CYAN}${desc}${NC} (${tag})" + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + local result_file="${RESULTS_DIR}/${safe_tag}.result" + local log_file="${RESULTS_DIR}/${safe_tag}.log" + + mkdir -p "$RESULTS_DIR" + + # Generate Dockerfile + generate_dockerfile "$tag" "$pkg" > "$RESULTS_DIR/Dockerfile.${safe_tag}" + + local start_time=$(date +%s) + + # Build with Docker + if docker build \ + -f "$RESULTS_DIR/Dockerfile.${safe_tag}" \ + -t "$image_name" \ + --no-cache \ + "$REPO_DIR" \ + > "$log_file" 2>&1; then + + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + + echo -e " ${GREEN}✓ PASS${NC} — ${duration}s" + echo "PASS:${duration}s" > "$result_file" + + # Clean up image to save disk + docker rmi "$image_name" 2>/dev/null || true + return 0 + else + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + + echo -e " ${RED}✗ FAIL${NC} — ${duration}s" + echo "FAIL:${duration}s" > "$result_file" + + # Show relevant error lines + echo -e " ${YELLOW}Error output:${NC}" + grep -E "(error:|Error:|fatal:|FAILED|No such)" "$log_file" | tail -5 | while read line; do + echo -e " ${RED}$line${NC}" + done + echo -e " ${YELLOW}Full log: ${log_file}${NC}" + return 1 + fi +} + +# Parse arguments +SINGLE="" +while [[ $# -gt 0 ]]; do + case $1 in + --clean) clean ;; + --list) list_distros; exit 0 ;; + --single) SINGLE="$2"; shift 2 ;; + -h|--help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +# Ensure Docker is available +if ! command -v docker &>/dev/null; then + echo -e "${RED}Error: Docker is not installed or not in PATH${NC}" + exit 1 +fi + +if ! docker info &>/dev/null; then + echo -e "${RED}Error: Docker daemon is not running${NC}" + exit 1 +fi + +# Header +echo "" +echo -e "${BOLD}╔═══════════════════════════════════════════════════════╗${NC}" +echo -e "${BOLD}║ sudosh2 Docker Build Test Suite ║${NC}" +echo -e "${BOLD}╚═══════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e " Repo: ${REPO_DIR}" +echo -e " Distros: ${#DISTROS[@]}" +echo -e " Results: ${RESULTS_DIR}/" +echo "" + +# Run tests +PASS=0 +FAIL=0 +TOTAL=0 + +if [ -n "$SINGLE" ]; then + index="$SINGLE" + if [ "$index" -lt 1 ] || [ "$index" -gt "${#DISTROS[@]}" ]; then + echo -e "${RED}Invalid test number. Use --list to see available tests.${NC}" + exit 1 + fi + entry="${DISTROS[$((index-1))]}" + if run_test "$index" "$entry"; then + ((PASS++)) + else + ((FAIL++)) + fi + ((TOTAL++)) +else + i=1 + for entry in "${DISTROS[@]}"; do + if run_test "$i" "$entry"; then + ((PASS++)) + else + ((FAIL++)) + fi + ((TOTAL++)) + ((i++)) + done +fi + +# Summary +echo "" +echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BOLD} Summary: ${GREEN}${PASS} passed${NC}, ${RED}${FAIL} failed${NC}, ${TOTAL} total" +echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" + +# Detailed results table +echo -e "${BOLD} Results by distribution:${NC}" +echo "" +printf " ${BOLD}%-30s %-8s %-8s %-10s${NC}\n" "Distribution" "Status" "Time" "Image" +echo " ─────────────────────────────────────────────────────────" + +if [ -n "$SINGLE" ]; then + entries=("${DISTROS[$((SINGLE-1))]}") +else + entries=("${DISTROS[@]}") +fi + +for entry in "${entries[@]}"; do + IFS='|' read -r tag pkg desc <<< "$entry" + safe_tag=$(echo "$tag" | tr '/:' '-') + result_file="${RESULTS_DIR}/${safe_tag}.result" + + if [ -f "$result_file" ]; then + result=$(cat "$result_file") + status="${result%%:*}" + timing="${result##*:}" + + if [ "$status" = "PASS" ]; then + printf " %-30s ${GREEN}%-8s${NC} %-8s %-10s\n" "$desc" "PASS" "$timing" "$tag" + else + printf " %-30s ${RED}%-8s${NC} %-8s %-10s\n" "$desc" "FAIL" "$timing" "$tag" + fi + fi +done + +echo "" + +# Save summary +cat > "${RESULTS_DIR}/summary.txt" < #endif /* GNU C library. */ +/* + * Modern systems provide getopt() in libc. Skip the entire bundled + * implementation to avoid symbol conflicts and duplicate definitions. + * Only compile bundled getopt on truly ancient systems. + */ +#if !defined(__GNU_LIBRARY__) && !defined(__linux__) && \ + !defined(__FreeBSD__) && !defined(__OpenBSD__) && \ + !defined(__NetBSD__) && !defined(__APPLE__) && \ + !defined(__DragonFly__) + /* This version of `getopt' appears to the caller like standard Unix `getopt' but it behaves differently for the user, since it allows the user - to intersperse the options with the other arguments. - - As `getopt' works, it permutes the elements of ARGV so that, - when it is done, all the options precede everything else. Thus - all application programs are extended to handle flexible argument order. - - Setting the environment variable POSIXLY_CORRECT disables permutation. - Then the behavior is completely standard. - - GNU application programs can use a third alternative mode in which - they can distinguish the relative order of options and other arguments. */ + to intersperse the options with the other arguments. */ #include "getopt.h" @@ -682,3 +682,5 @@ getopt (argc, argv, optstring) } #endif /* _LIBC or not __GNU_LIBRARY__. */ + +#endif /* Modern systems — skip bundled getopt entirely */ diff --git a/src/getopt.h b/src/getopt.h index c1028a7..f423e1c 100644 --- a/src/getopt.h +++ b/src/getopt.h @@ -22,6 +22,32 @@ extern "C" { #endif +/* + * Modern systems (glibc 2.x, musl, BSDs, macOS) all provide getopt, + * getopt_long, and getopt_long_only in their system headers via + * or . Only use our bundled declarations on truly ancient systems + * that lack native getopt support. + * + * The old code declared `extern int getopt();` (K&R, no prototype) which + * conflicts with modern glibc's `int getopt(int, char *const *, const char *)` + * when both getopt.h and are included (gcc-15/glibc-2.39+). + * + * Fixes: https://github.com/WLTBAgent/sudosh2/issues/53 + */ +#if defined(__GNU_LIBRARY__) || defined(__linux__) || defined(__FreeBSD__) || \ + defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) || \ + defined(__DragonFly__) + +/* Use system getopt — provides getopt on glibc/musl systems. + * Some systems have a separate for getopt_long. */ +#include +#ifdef HAVE_GETOPT_H +#include +#endif + +#else +/* Legacy/ancient systems — use bundled getopt declarations */ + /* For communication from `getopt' to the caller. When `getopt' finds an option that takes an argument, the argument value is returned here. @@ -94,14 +120,7 @@ extern "C" { #define optional_argument 2 #if __STDC__ -#if defined(__GNU_LIBRARY__) -/* Many other libraries have conflicting prototypes for getopt, with - differences in the consts, in stdlib.h. To avoid compilation - errors, only prototype getopt for the GNU C library. */ extern int getopt(int argc, char *const *argv, const char *shortopts); -#else /* not __GNU_LIBRARY__ */ - extern int getopt(); -#endif /* not __GNU_LIBRARY__ */ extern int getopt_long(int argc, char *const *argv, const char *shortopts, const struct option *longopts, int *longind); @@ -110,7 +129,7 @@ extern "C" { const struct option *longopts, int *longind); -/* Internal only. Users should not call this directly. */ +/* Internal only. Users should not call this directly. */ extern int _getopt_internal(int argc, char *const *argv, const char *shortopts, const struct option *longopts, @@ -123,6 +142,8 @@ extern "C" { extern int _getopt_internal(); #endif /* not __STDC__ */ +#endif /* modern vs legacy systems */ + #ifdef __cplusplus } #endif -- 2.53.0