#!/usr/bin/env python3 """Linux build path for nsis7z plugin. Builds Windows DLL artifacts from Linux using MinGW-w64 cross compilers. Supported versions: 7-Zip 25.01, 26.00 and zstd. All project-owned build-infrastructure files (makefile.gcc, Nsis7z.def) live under tools/linux/overlay/ and are never written into the vendor source trees. Build artifacts go to _linux_build/// (gitignored). """ from __future__ import annotations import argparse import os import shutil import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] OVERLAY = ROOT / "tools" / "linux" / "overlay" BUILD_DIR = ROOT / "_linux_build" SUPPORTED = {"25.01", "26.00", "26.01", "zstd"} # For all versions the upstream vendor does NOT ship a Nsis7z bundle, so a # *-bundle wrapper directory mirrors the vendor tree via symlinks and provides # project-owned sources (NSIS UI, wrapper cpp files, vcxproj). # # For zstd the 7-zip C++ sources live in the 7-zip-zstd submodule while our # NSIS wrapper sits in versions/zstd/. make is run with -C pointing to the # wrapper dir and VENDOR_7ZIP overridden to the submodule tree so that # include/source rules resolve correctly without touching the submodule. VERSION_LAYOUT = { "25.01": { "vendor_7zip": ROOT / "versions" / "25.01" / "CPP" / "7zip", "bundle_dir": ROOT / "versions" / "25.01-bundle" / "CPP" / "7zip" / "Bundles" / "Nsis7z", }, "26.00": { "vendor_7zip": ROOT / "versions" / "26.00" / "CPP" / "7zip", "bundle_dir": ROOT / "versions" / "26.00-bundle" / "CPP" / "7zip" / "Bundles" / "Nsis7z", }, "26.01": { "vendor_7zip": ROOT / "versions" / "26.01" / "CPP" / "7zip", "bundle_dir": ROOT / "versions" / "26.01-bundle" / "CPP" / "7zip" / "Bundles" / "Nsis7z", }, # zstd: make runs in our wrapper dir; vendor_7zip points to submodule tree. "zstd": { "vendor_7zip": ROOT / "versions" / "7-zip-zstd" / "CPP" / "7zip", "bundle_dir": ROOT / "versions" / "zstd" / "CPP" / "7zip" / "Bundles" / "Nsis7z", }, } # Per-version C++-only extra flags for GCC/MinGW cross-compilation. # These compensate for vendor code written exclusively for MSVC. # Passed via LOCAL_CXXFLAGS_EXTRA → appended to CXXFLAGS only (not CFLAGS). # # If flag-based suppression is insufficient (e.g. the code must be modified), # place a patched copy at tools/linux/overlay/src// # and add a rule override in makefile.gcc using the OVERLAY_SRC variable. VERSION_EXTRA_CXXFLAGS: dict[str, str] = { # 7-zip 25.01 NSIS UI code was never compiled with GCC: # - STDMETHODIMP implementations lack `noexcept`/`throw()` while the COM # interface macros (Z7_COM7F_IMP) declare the virtuals with `throw()` # (= noexcept in C++17). Fixed per-TU via -include z7_idecl_noexcept_strip.h # in the specific makefile rule for NsisExtractCallbackConsole.o. # - Several unused parameters and sign-compare on UInt64 vs. literal -1. "25.01": "-Wno-sign-compare -Wno-unused-parameter", } CONFIGS = { "x86-ansi": { "triplet": "i686-w64-mingw32", "is_x64": "0", "unicode": False, "dest": "plugins/x86-ansi/nsis7z.dll", }, "x86-unicode": { "triplet": "i686-w64-mingw32", "is_x64": "0", "unicode": True, "dest": "plugins/x86-unicode/nsis7z.dll", }, "x64-unicode": { "triplet": "x86_64-w64-mingw32", "is_x64": "1", "unicode": True, "dest": "plugins/amd64-unicode/nsis7z.dll", }, } def _require_tool(name: str) -> None: if shutil.which(name) is None: print(f"ERROR: required tool not found: {name}", file=sys.stderr) sys.exit(2) def _resolve_jobs(value: str) -> int: if value == "auto": return max(1, os.cpu_count() or 1) jobs = int(value) if jobs < 1: raise ValueError("jobs must be >= 1") return jobs def _ensure_bundle_symlinks(zip_version: str, bundle_dir: Path, vendor_7zip: Path) -> None: """Recreate bundle symlinks if they are missing (e.g. after a fresh clone). Symlinks are no longer committed to git (they are POSIX-only and break Windows clones), so they must be created on-the-fly on Linux before make can resolve the relative source paths inside the bundle directory. """ # A sentinel: if the vendor C/ directory is not reachable via the bundle # root symlink, assume all symlinks need to be (re)created. sentinel = bundle_dir.parents[3] / "C" # bundle/CPP/7zip/../../../C = bundle/C if sentinel.exists(): return # symlinks already in place setup_script = ROOT / "tools" / "linux" / "setup_bundle_symlinks.py" print(f"[linux] Bundle symlinks missing for {zip_version}, recreating via {setup_script.name} ...") proc = subprocess.run( [sys.executable, str(setup_script), zip_version], cwd=ROOT, ) if proc.returncode != 0: print(f"ERROR: setup_bundle_symlinks.py failed for {zip_version}", file=sys.stderr) sys.exit(proc.returncode) def _build_one( zip_version: str, cfg_name: str, verbose: bool, jobs: int, clean: bool, cleanup_artifacts: bool, ) -> int: layout = VERSION_LAYOUT[zip_version] vendor_7zip = layout["vendor_7zip"] bundle_dir = layout["bundle_dir"] # Symlinks are not committed to git; recreate them on Linux if missing. if zip_version in VERSION_LAYOUT and zip_version != "zstd": _ensure_bundle_symlinks(zip_version, bundle_dir, vendor_7zip) cfg = CONFIGS[cfg_name] triplet = cfg["triplet"] # Output objects go to an absolute path under _linux_build/ so nothing # temporary is ever written into the vendor source trees. out_dir = BUILD_DIR / zip_version / cfg_name out_dir.mkdir(parents=True, exist_ok=True) local_flags = "-DNDEBUG" if cfg["unicode"]: local_flags += " -DUNICODE -D_UNICODE" else: # Precomp.h forces UNICODE=1 unconditionally; suppress it for the ansi # build so that LPTSTR resolves to char* consistently in all TUs. local_flags += " -DZ7_NO_UNICODE" # Version-specific workarounds for vendor code that was MSVC-only. # Flags that are C++-only (e.g. -fpermissive) must go into LOCAL_CXXFLAGS_EXTRA # which the makefile appends only to CXXFLAGS, not to CFLAGS. extra_cxx = VERSION_EXTRA_CXXFLAGS.get(zip_version, "") # CXX_INCLUDE_FLAGS: # 1. overlay/include – case-sensitivity shims (Windows.h → windows.h, …) # searched first so they shadow the actual MinGW system headers. # 2. CPP root – so vendor #include paths resolve from any working dir. cpp_root = vendor_7zip.parent # …/CPP overlay_inc = OVERLAY / "include" # For bundle-based builds (zstd, 26.01) the UI/NSIS wrapper files live in # our bundle dir, not in the vendor tree. Override NSIS_DIR accordingly. bundle_nsis = bundle_dir.parent.parent / "UI" / "NSIS" base_cmd = [ "make", "-C", str(bundle_dir), "-f", str(OVERLAY / "makefile.gcc"), f"O={out_dir}", f"DEF_FILE={OVERLAY / 'Nsis7z.def'}", f"VENDOR_7ZIP={vendor_7zip}", *([ f"NSIS_DIR={bundle_nsis}" ] if bundle_nsis.is_dir() else []), "SystemDrive=1", "IS_MINGW=1", "MSYSTEM=LINUX", f"IS_X64={cfg['is_x64']}", f"CC={triplet}-gcc", f"CXX={triplet}-g++", f"RC={triplet}-windres", f"LOCAL_FLAGS_EXTRA={local_flags} -Wno-unknown-pragmas", f"LOCAL_CXXFLAGS_EXTRA={extra_cxx}", # Linux ld is case-sensitive; vendor makefile uses mixed-case lib names # (lUser32, lOle32, …) that don't exist on disk. Override LIB2 with # fully-expanded lowercase equivalents. "LIB2=-loleaut32 -luuid -ladvapi32 -luser32 -lole32 -lgdi32 -lcomctl32 -lcomdlg32 -lshell32 -lhtmlhelp", f"CXX_INCLUDE_FLAGS=-I{overlay_inc} -I{cpp_root}", ] build_mode = "clean build" if clean else "incremental build" print(f"[linux] Building {cfg_name} ({zip_version}, {build_mode}, -j{jobs})") # Don't run 'clean' and 'all' in the same parallel make invocation: # with -j, GNU make may execute them concurrently and remove objects while # the linker is already consuming them. if clean: proc = subprocess.run(base_cmd + ["clean"], cwd=bundle_dir) if proc.returncode != 0: return proc.returncode proc = subprocess.run(base_cmd + [f"-j{jobs}", "all"], cwd=bundle_dir) if proc.returncode != 0: return proc.returncode built = out_dir / "nsis7z.dll" if not built.exists(): print(f"ERROR: expected artifact not found: {built}", file=sys.stderr) return 1 dest = ROOT / cfg["dest"] dest.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(built, dest) if cleanup_artifacts: shutil.rmtree(out_dir, ignore_errors=True) if verbose: print(f"[linux] Copied -> {dest}") if cleanup_artifacts: print(f"[linux] Cleaned build artifacts -> {out_dir}") return 0 def main() -> int: parser = argparse.ArgumentParser(description="Linux build path for nsis7z") parser.add_argument( "--7zip-version", dest="zip_version", choices=sorted(SUPPORTED), default="26.00", help="7-Zip version to build", ) parser.add_argument( "--configs", nargs="+", choices=list(CONFIGS.keys()) + ["all"], default=["all"], help="Build configuration(s): x86-ansi, x86-unicode, x64-unicode, or all", ) parser.add_argument( "--jobs", default="auto", help="Parallel make jobs. Use an integer or 'auto' (default: auto)", ) parser.add_argument( "--clean", action="store_true", default=True, help="Run 'make clean' before building (default: True)", ) parser.add_argument( "--no-clean", action="store_false", dest="clean", help="Skip 'make clean' to enable incremental rebuilds", ) parser.add_argument( "--cleanup-artifacts", action="store_true", default=True, help="Remove per-config build artifact directories after successful copy (default: True)", ) parser.add_argument( "--no-cleanup-artifacts", action="store_false", dest="cleanup_artifacts", help="Keep per-config build artifact directories (_o_*)", ) parser.add_argument("--verbose", action="store_true", help="Verbose output") args = parser.parse_args() _require_tool("make") for cfg in CONFIGS.values(): _require_tool(f"{cfg['triplet']}-gcc") _require_tool(f"{cfg['triplet']}-g++") _require_tool(f"{cfg['triplet']}-windres") try: jobs = _resolve_jobs(args.jobs) except ValueError as exc: print(f"ERROR: invalid --jobs value: {exc}", file=sys.stderr) return 2 wanted = list(CONFIGS.keys()) if "all" in args.configs else args.configs for cfg_name in wanted: code = _build_one( args.zip_version, cfg_name, args.verbose, jobs, args.clean, args.cleanup_artifacts, ) if code != 0: return code print("[linux] Build completed") return 0 if __name__ == "__main__": sys.exit(main())