diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index fd8f167..132fa08 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -91,7 +91,7 @@ jobs: path: ./wheelhouse/*.whl if-no-files-found: error - windows_arm64_wheels: # Not tested + windows_arm64_wheels: # TODO: for later: use windows-11-arm runners as they're available now name: arm64-windows runs-on: windows-latest permissions: @@ -108,6 +108,13 @@ jobs: cache: false check-latest: true + - name: Resolve Windows Meson cross file + id: meson-cross-file + shell: pwsh + run: | + $crossFile = (Resolve-Path (Join-Path $env:GITHUB_WORKSPACE 'meson_cross_files/windows-arm64.ini')).Path -replace '\\', '/' + "path=$crossFile" >> $env:GITHUB_OUTPUT + - name: Build binary distribution (wheel) on Windows (arm64) # We need to use cibuildwheel because it has experimental support for cross-compiling # to arm64 and setup-python does not have arm64 support on Windows right now @@ -115,14 +122,11 @@ jobs: with: package-dir: . output-dir: wheelhouse - # Cross-compile for arm64 target via Zig toolchain env: - USE_ZIG: "1" - GOOS: windows - GOARCH: arm64 CIBW_BUILD: "cp312-*" CIBW_ARCHS_WINDOWS: ARM64 CIBW_BEFORE_BUILD_WINDOWS: "pip install delvewheel" + CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--cross-file=${{ steps.meson-cross-file.outputs.path }}" CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair -w {dest_dir} {wheel} CIBW_TEST_SKIP: "*-win_arm64" @@ -149,21 +153,23 @@ jobs: go-version: "1.26.1" cache: false - # Note: cibuildwheel will manage installing 32-bit Python on Windows. We - # do not need to do that manually unless we use setup-python instead. + - name: Resolve Windows Meson cross file + id: meson-cross-file + shell: pwsh + run: | + $crossFile = (Resolve-Path (Join-Path $env:GITHUB_WORKSPACE 'meson_cross_files/windows-386.ini')).Path -replace '\\', '/' + "path=$crossFile" >> $env:GITHUB_OUTPUT + - name: Build binary distribution (wheel) on Windows (i686) uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 with: package-dir: . output-dir: wheelhouse - # Cross-compile for i686 target via Zig toolchain env: - USE_ZIG: "1" - GOOS: windows - GOARCH: 386 CIBW_BUILD: "cp312-*" CIBW_ARCHS_WINDOWS: x86 CIBW_BEFORE_BUILD_WINDOWS: "pip install delvewheel" + CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--cross-file=${{ steps.meson-cross-file.outputs.path }}" CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair -w {dest_dir} {wheel} CIBW_TEST_COMMAND: | hugo version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4899096..b0943a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,42 @@ jobs: - name: Run style checks uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1 + editable_install: + needs: [style] + name: editable-install-smoke + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + submodules: recursive + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.14" + + - name: Set up Go toolchain + id: setup-go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: "1.26.1" + cache: false + check-latest: true + + - name: Restore Hugo builder cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: ~/.cache/hugo-go + key: editable-install-hugo-${{ runner.os }}-${{ steps.setup-go.outputs.go-version }} + + - name: Install Python dependencies + run: python -m pip install nox[uv] + + - name: Smoke test editable install entry points + run: nox -s editable + build_wheels: needs: [style] name: ${{ matrix.runs-on }}-python-${{ matrix.python-version }} @@ -79,7 +115,7 @@ jobs: - name: Restore Hugo builder cache uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: - path: ./hugo_cache/ + path: ~/.cache/hugo-go key: ${{ matrix.runs-on }}-hugo-${{ steps.setup-go.outputs.go-version }} - name: Install Python dependencies @@ -136,7 +172,7 @@ jobs: - name: Restore Hugo builder cache uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: - path: ./hugo_cache/ + path: ~/.cache/hugo-go key: zig-${{ matrix.runs-on }}-${{ matrix.architecture}}-hugo-experimental-${{ steps.setup-go.outputs.go-version }} - name: Install Python dependencies @@ -144,32 +180,33 @@ jobs: - name: Build binary distribution (wheel) on Linux if: matrix.runs-on == 'ubuntu-latest' - # Cross-compile for arm64 target via Zig toolchain - env: - USE_ZIG: 1 - GOOS: linux - GOARCH: arm64 - run: | - python -m build --wheel . --outdir wheelhouse/ + run: python -m build --wheel . --outdir wheelhouse -Csetup-args=--cross-file="$GITHUB_WORKSPACE/meson_cross_files/linux-arm64.ini" # can't repair arm64 wheels on Linux x86_64 right now # auditwheel repair --plat manylinux_2_28_aarch64 -w wheelhouse/ dist/*.whl + - name: Resolve Windows Meson cross file + if: matrix.runs-on == 'windows-latest' + id: meson-cross-file + shell: pwsh + env: + MATRIX_ARCH: ${{ matrix.architecture }} + run: | + $crossFileName = if ($env:MATRIX_ARCH -eq 'arm64') { 'windows-arm64.ini' } else { 'windows-386.ini' } + $crossFile = (Resolve-Path (Join-Path $env:GITHUB_WORKSPACE "meson_cross_files/$crossFileName")).Path -replace '\\', '/' + "path=$crossFile" >> $env:GITHUB_OUTPUT + - name: Build binary distribution (wheel) on Windows (arm64) if: matrix.runs-on == 'windows-latest' && matrix.architecture == 'arm64' - # We need to use cibuildwheel because it has experimental support for cross-compiling - # to arm64 and setup-python does not have arm64 support on Windows right now + # TODO: FIXME: use windows-11-arm runners as they're available now, and drop this uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 with: package-dir: . output-dir: wheelhouse - # Cross-compile for arm64 target via Zig toolchain env: - USE_ZIG: "1" - GOOS: windows - GOARCH: arm64 CIBW_BUILD: "cp312-*" CIBW_ARCHS_WINDOWS: ARM64 CIBW_BEFORE_BUILD_WINDOWS: "pip install delvewheel" + CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--cross-file=${{ steps.meson-cross-file.outputs.path }}" CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair -w {dest_dir} {wheel} CIBW_TEST_SKIP: "*-win_arm64" @@ -181,14 +218,11 @@ jobs: with: package-dir: . output-dir: wheelhouse - # Cross-compile for i686 target via Zig toolchain env: - USE_ZIG: "1" - GOOS: windows - GOARCH: 386 CIBW_BUILD: "cp312-*" CIBW_ARCHS_WINDOWS: x86 CIBW_BEFORE_BUILD_WINDOWS: "pip install delvewheel" + CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--cross-file=${{ steps.meson-cross-file.outputs.path }}" CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair -w {dest_dir} {wheel} CIBW_TEST_COMMAND: | hugo version diff --git a/.github/workflows/update-hugo.yml b/.github/workflows/update-hugo.yml index 1fad65a..d779ef8 100644 --- a/.github/workflows/update-hugo.yml +++ b/.github/workflows/update-hugo.yml @@ -27,7 +27,7 @@ jobs: run: | set -euo pipefail - CURRENT_VERSION=$(grep -oP 'HUGO_VERSION = "\K[0-9.]+' setup.py) + CURRENT_VERSION=$(grep -oP "version\s*:\s*'\K[0-9.]+" meson.build) echo "Current version: $CURRENT_VERSION" # Find the next Hugo release after our current version @@ -60,8 +60,7 @@ jobs: RELEASE_TYPE="patch" fi - sed -i "s/HUGO_VERSION = \"$CURRENT_VERSION\"/HUGO_VERSION = \"$LATEST_VERSION\"/" setup.py - sed -i "s/HUGO_VERSION = \"$CURRENT_VERSION\"/HUGO_VERSION = \"$LATEST_VERSION\"/" src/hugo/cli.py + sed -i "s/version\s*:\s*'$CURRENT_VERSION'/version : '$LATEST_VERSION'/" meson.build # Update the Hugo submodule to the new version tag cd hugo @@ -88,7 +87,7 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/$GITHUB_REPOSITORY" git checkout -b "update-hugo-v${LATEST_VERSION}" - git add setup.py src/hugo/cli.py hugo + git add meson.build hugo git commit -m "Update Hugo to v${LATEST_VERSION}" git push origin "update-hugo-v${LATEST_VERSION}" gh pr create \ diff --git a/.gitignore b/.gitignore index 16ca7cf..c8fb513 100644 --- a/.gitignore +++ b/.gitignore @@ -182,13 +182,13 @@ src/hugo/binaries/* # Hugo builder cache hugo_cache/ -# Hugo version file -hugo/VERSION - # Documentation /docs/public /docs/resources *.lock # Generated stamp file for sdist builds -hugo/.hugo_commit_date +hugo-src/.hugo_commit_date + +# Generated by meson configure_file +src/hugo/_version.py diff --git a/.gitmodules b/.gitmodules index bb03a6e..c7323bd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "hugo"] - path = hugo + path = hugo-src url = https://github.com/gohugoio/hugo.git diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 8bd240a..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,14 +0,0 @@ -graft src/hugo -graft hugo -prune docs - -include LICENSE LICENSE-hugo.txt README.md CODE_OF_CONDUCT.md SECURITY.md pyproject.toml setup.py setup.cfg -exclude src/hugo/binaries/hugo-* - -# Exclude some files from the Hugo submodule that are too large that they -# inflate the sdist. They are not needed for building. -prune hugo/docs -prune hugo/testscripts -recursive-exclude hugo testdata/* - -global-exclude __pycache__ *.py[cod] *.so *.dylib .DS_Store .venv .git diff --git a/docs/content/building-from-sources.md b/docs/content/building-from-sources.md index d005d4a..20899b1 100644 --- a/docs/content/building-from-sources.md +++ b/docs/content/building-from-sources.md @@ -5,11 +5,15 @@ draft = false toc = true +++ -Building the extended + withdeploy edition of Hugo from source requires the following dependencies: +The build is driven by [Meson](https://mesonbuild.com/) and [meson-python](https://meson-python.readthedocs.io/). Building the extended + withdeploy edition of Hugo from source requires the following dependencies: 1. The [Go](https://go.dev/doc/install) toolchain 2. The [Git](https://git-scm.com/downloads) version control system -3. A C/C++ compiler, such as [GCC](https://gcc.gnu.org/) or [Clang](https://clang.llvm.org/) +3. A C/C++ compiler, such as [GCC](https://gcc.gnu.org/) or [Clang](https://clang.llvm.org/). You may also use [Zig](https://ziglang.org/) as a C compiler. On Windows, the [MinGW](https://www.mingw-w64.org/) toolchain is supported, and [MSVC](https://visualstudio.microsoft.com/visual-cpp-build-tools/) is untested. + 3a. For cross-compilation to non-macOS targets, [Zig](https://ziglang.org/) is pulled in from PyPI and auto-selected as the C compiler. For cross-compilation from macOS hosts to macOS targets, AppleClang is used with `-arch `. +4. [Python](https://www.python.org/downloads/) ≥ 3.10 + +`meson-python`, `meson`, `ninja`, and `ziglang` are all pulled in as build-time dependencies by the build backend, so you don't have to install them yourself. Windows users can use the [Chocolatey package manager](https://chocolatey.org/) in order to use the [MinGW compiler](https://chocolatey.org/packages/mingw). After installing Chocolatey, run the following command in an elevated terminal prompt: @@ -17,16 +21,18 @@ Windows users can use the [Chocolatey package manager](https://chocolatey.org/) choco install mingw ``` -Then, clone the repository and run the build script: +Then, clone the repository and install the package: {{< tabs >}} {{< tab name="Linux/macOS" >}} ```bash -git clone --recurse-submodules https://github.com/agriyakhetarpal/hugo-python-distributions@main +git clone --recurse-submodules https://github.com/agriyakhetarpal/hugo-python-distributions +cd hugo-python-distributions python -m venv venv source venv/bin/activate +pip install . ``` {{< /tab >}} @@ -34,22 +40,18 @@ source venv/bin/activate {{< tab name="Windows" >}} ```cmd -git clone --recurse-submodules https://github.com/agriyakhetarpal/hugo-python-distributions@main +git clone --recurse-submodules https://github.com/agriyakhetarpal/hugo-python-distributions +cd hugo-python-distributions py -m venv venv venv\Scripts\activate.bat +pip install . ``` {{< /tab >}} {{< /tabs >}} -and then install the package in the current directory: - -```bash -pip install . -``` - -or perform an editable installation via the following command: +For an editable install: ```bash pip install -e . @@ -61,89 +63,87 @@ pip install -e . Cross-compilation is experimental and may not be stable or reliable for all use cases. If you encounter any issues, please feel free to [open an issue](https://github.com/agriyakhetarpal/hugo-python-distributions/issues/new). {{}} -This project is capable of cross-compiling Hugo binaries for various platforms and architectures. Cross-compilation is provided for the following platforms: +Cross-compilation is indicated by and happens entirely via a [Meson cross file](https://mesonbuild.com/Cross-compilation.html). This cross-build definition file has a `[host_machine]` section that describes the target platform, and Meson and meson-python both consume it: -1. macOS; for the `arm64` and `amd64` architectures via the Xcode toolchain, -2. Linux; for the `arm64`, `amd64`, `s390x`, and `ppc64le` architectures via the Zig toolchain, and -3. Windows; for the `amd64`, `arm64`, and `x86` architectures via the Zig toolchain. +- `meson` gets to know what the target is, +- `meson.build` gets to know what `GOOS`/`GOARCH` combination is to be passed on to the Go build, and whether to use the Zig compiler for cross-compilation or not, based on said combination. +- There is an in-tree PEP 517 build backend wrapper around `meson-python` at [`scripts/hugo_meson_python_wrapper.py`](https://github.com/agriyakhetarpal/hugo-python-distributions/blob/main/scripts/hugo_meson_python_wrapper.py), that sets `_PYTHON_HOST_PLATFORM` to tag the wheel correctly for cross-compilation scenarios, both + across platforms or across architectures on the same platform. -{{< tabs >}} +### 1. Generate a cross file -{{< tab name="macOS" >}} -Say, on an Intel-based (x86_64) macOS machine: +There is a helper that ships with the project: ```bash -export GOARCH="arm64" -pip install . # or pip install -e . +python scripts/generate_meson_cross.py --goos linux --goarch arm64 --output cross-linux-arm64.ini ``` -This will build a macOS `arm64` binary distribution of Hugo that can be used on Apple Silicon-based (`arm64`) macOS machines. To build a binary distribution for the _target_ Intel-based (`x86_64`) macOS platform on the _host_ Apple Silicon-based (`arm64`) macOS machine, you can use the following command: +The output is a small `[host_machine]` description that meson-python and Meson both consume. You can also write one by hand if you'd like – see the [Meson cross file documentation](https://mesonbuild.com/Cross-compilation.html#cross-file). -```bash -export GOARCH="amd64" -pip install . # or pip install -e . -``` +Some cross files are already checked into the repository for convenience, in the `meson_cross_files` directory. You can use those directly, or as a reference for writing your own. -{{< /tab >}} +Cross-compilation for Hugo binaries is provided for the following platforms and architectures, based on `GOOS`/`GOARCH` combinations that the Go toolchain supports, and that Zig can target for C code generation: + +- macOS: `darwin/arm64` and `darwin/amd64` +- Linux: `linux/amd64`, `linux/arm64`, `linux/arm`, `linux/386`, `linux/ppc64le`, `linux/s390x`, and `linux/riscv64` +- Windows: `windows/amd64`, `windows/arm64`, and `windows/386` -{{< tab name="Linux" >}} -Set the `USE_ZIG`, `GOOS`, and `GOARCH` environment variables prior to installing the package: +For a list of supported distributions for Go, please run the `go tool dist list` command on your system. For a list of supported targets for Zig, please refer to the [Zig documentation](https://ziglang.org/documentation/) for more information or run the `zig targets` command on your system. -Say, on an `amd64` Linux machine: +### 2. Build the wheel ```bash -export USE_ZIG="1" -export GOOS="linux" -export GOARCH="arm64" -pip install . # or pip install -e . +python -m build --wheel -Csetup-args=--cross-file=cross-linux-arm64.ini ``` -will cross-compile a Linux arm64 binary distribution of Hugo that can be used on the targeted arm64 Linux machines. To build a binary distribution for the _target_ `amd64` Linux platform on the _host_ `arm64` Linux machine, set the targets differently: +Some examples are showcased below. -```bash -export USE_ZIG="1" -export GOOS="linux" -export GOARCH="amd64" -pip install . # or pip install -e . -``` +{{< tabs >}} -This creates dynamic linkage for the built Hugo binary with a system-provided GLIBC. If you wish to statically link the binary with MUSL, change the `CC` and `CXX` environment variables as follows: +{{< tab name="Linux arm64" >}} ```bash -export CC="zig cc -target x86_64-linux-musl" -export CXX="zig c++ -target x86_64-linux-musl" +python scripts/generate_meson_cross.py --goos linux --goarch arm64 --output cross.ini +python -m build --wheel -Csetup-args=--cross-file=cross.ini +# builds into dist/hugo--py3-none-linux_aarch64.whl ``` -Linkage against MUSL is not tested in CI at this time, but it should work in theory. The official Hugo binaries do not link against MUSL for a variety of reasons including the size of the binary and the prevalence of the GLIBC C standard library. {{< /tab >}} -{{< tab name="Windows" >}} -Set these environment variables prior to installing the package (note the use of `set` instead of `export` on Windows): - -Say, on an `amd64` Windows machine: +{{< tab name="Windows arm64" >}} -```cmd -set USE_ZIG="1" -set GOOS="windows" -set GOARCH="arm64" -pip install . # or pip install -e . +```bash +python scripts/generate_meson_cross.py --goos windows --goarch arm64 --output cross.ini +python -m build --wheel -Csetup-args=--cross-file=cross.ini +# builds into dist/hugo--py3-none-win_arm64.whl ``` -will cross-compile a Windows `arm64` binary distribution of Hugo that can be used on the targeted `arm64` Windows machines, and so on for the `x86` architecture: +{{< /tab >}} -```cmd -set USE_ZIG="1" -set GOOS="windows" -set GOARCH="386" -pip install . # or pip install -e . +{{< tab name="macOS arm64 to macOS x86_64" >}} + +```bash +python scripts/generate_meson_cross.py --goos darwin --goarch amd64 --output cross.ini +python -m build --wheel -Csetup-args=--cross-file=cross.ini +# builds into dist/hugo--py3-none-macosx_26_0_x86_64.whl ``` +For darwin to darwin cross-builds, you may use AppleClang `-arch `. + {{< /tab >}} {{< /tabs >}} -For a list of supported distributions for Go, please run the `go tool dist list` command on your system. For a list of supported targets for Zig, please refer to the [Zig documentation](https://ziglang.org/documentation/) for more information or run the `zig targets` command on your system. +### Use a custom toolchain (such as, for MUSL on Linux) -{{< callout type="info" >}} -Cross-compilation for a target platform and architecture from a different host platform and architecture is also possible, but it remains largely untested at this time. Currently, the [Zig compiler toolchain](https://ziglang.org/) is known to work for cross-platform, cross-architecture compilation. -{{}} +To override the auto-selected compiler, say, to link against MUSL instead of GLIBC on Linux, you can set `CC`/`CXX` manually and disable the Zig target auto-selection by the build backend: + +```bash +export CC="$(python -m ziglang) cc -target x86_64-linux-musl" +export CXX="$(python -m ziglang) c++ -target x86_64-linux-musl" +python -m build --wheel \ + -Csetup-args=--cross-file=cross-linux-amd64.ini \ + -Csetup-args=-Duse_zig=disabled +``` + +Linkage against MUSL is not tested in CI at this time, but it should work in theory. The official Hugo binaries do not link against MUSL for a variety of reasons including the size of the binary and the prevalence of the GLIBC C standard library. diff --git a/hugo b/hugo-src similarity index 100% rename from hugo rename to hugo-src diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..0bb5a5a --- /dev/null +++ b/meson.build @@ -0,0 +1,105 @@ +project( + 'hugo-python-distributions', + version : '0.160.1', + meson_version : '>=1.3.0', + default_options : ['warning_level=0'], +) + +# Setting `pure: true` routes the Python package to purelib, so platlib stays +# empty. The bundled Hugo executable is installed under datadir and the marker +# file below keeps the wheel tag at py3-none- instead of a +# Python-version-specific ABI tag. +# See _has_extension_modules and _pure in mesonpy/__init__.py +py = import('python').find_installation(pure: true) + +go = find_program('go', required: true) +git = find_program('git', required: true) + +configure_file( + input : 'src/hugo/_version.py.in', + output : '_version.py', + configuration : { 'HUGO_VERSION' : meson.project_version() }, + install : true, + install_dir : py.get_install_dir() / 'hugo', + install_tag : 'python-runtime', +) + +py.install_sources( + ['src/hugo/__main__.py', 'src/hugo/cli.py'], + subdir : 'hugo', +) + +host_sys = host_machine.system() +host_cpu = host_machine.cpu_family() +exe_suffix = host_sys == 'windows' ? '.exe' : '' + +# Map meson's host_machine values to Golang's GOOS/GOARCH values +# for cross-compilation +GOOS_MAP = { + 'linux' : 'linux', + 'darwin' : 'darwin', + 'windows' : 'windows', +} +GOARCH_MAP = { + 'x86_64' : 'amd64', + 'aarch64' : 'arm64', + 'arm' : 'arm', + 'x86' : '386', + 'ppc64' : 'ppc64le', + 's390x' : 's390x', + 'riscv64' : 'riscv64', +} +if not GOOS_MAP.has_key(host_sys) + error('Unsupported host_machine.system: ' + host_sys) +endif +if not GOARCH_MAP.has_key(host_cpu) + error('Unsupported host_machine.cpu_family: ' + host_cpu) +endif +goos = GOOS_MAP[host_sys] +goarch = GOARCH_MAP[host_cpu] + +# Use Zig when cross-compiling, except for the "macOS arm64 to macOS x86_64" +# case where Zig lacks the macOS SDK and cannot find libresolv.dylib. For that +# we rely on the system linker as it is where AppleClang has native support +# for cross-compilation. This can still be overridden by setting the use_zig +# option to true or false explicitly. +auto_use_zig = meson.is_cross_build() and host_sys != 'darwin' +use_zig_opt = get_option('use_zig') +use_zig = use_zig_opt.auto() ? auto_use_zig : use_zig_opt.enabled() + + +custom_target( + 'hugo-binary', + output : 'hugo' + exe_suffix, # must match src/hugo/cli.py + command : [ + py.full_path(), + files('scripts/build_hugo.py'), + '--hugo-src', meson.project_source_root() / 'hugo-src', + '--cache', meson.project_build_root() / 'hugo_cache', + '--version', meson.project_version(), + '--output', '@OUTPUT@', + '--use-zig', use_zig.to_string(), + '--goos', goos, + '--goarch', goarch, + ], + install : true, + install_dir : get_option('datadir') / 'hugo' / 'binaries', + install_tag : 'python-runtime', + build_by_default : true, + console : true, +) + +# This is a hack to make meson-python mark the wheel as non-pure regardless +# of whether the host platform's magic-number check in mesonpy._is_native +# matches the target binary. _pure becomes False whenever mesonpy-libs +# ({libdir}) is non-empty, without running _is_native on its contents. See +#_WheelBuilder._pure in mesonpy/__init__.py. +configure_file( + output : '.platform_marker', + configuration : configuration_data(), + install : true, + install_dir : get_option('libdir'), + install_tag : 'runtime', +) + +meson.add_dist_script(py.full_path(), files('scripts/prune_sdist.py')) diff --git a/meson.options b/meson.options new file mode 100644 index 0000000..46ab0e9 --- /dev/null +++ b/meson.options @@ -0,0 +1,6 @@ +option( + 'use_zig', + type : 'feature', + value : 'auto', + description : 'Use ziglang as the CGO cross-compiler. "auto" picks Zig when cross-compiling to a non-darwin target.', +) diff --git a/meson_cross_files/linux-arm64.ini b/meson_cross_files/linux-arm64.ini new file mode 100644 index 0000000..de762c2 --- /dev/null +++ b/meson_cross_files/linux-arm64.ini @@ -0,0 +1,5 @@ +[host_machine] +system = 'linux' +cpu_family = 'aarch64' +cpu = 'aarch64' +endian = 'little' diff --git a/meson_cross_files/windows-386.ini b/meson_cross_files/windows-386.ini new file mode 100644 index 0000000..a3477d7 --- /dev/null +++ b/meson_cross_files/windows-386.ini @@ -0,0 +1,5 @@ +[host_machine] +system = 'windows' +cpu_family = 'x86' +cpu = 'i686' +endian = 'little' diff --git a/meson_cross_files/windows-arm64.ini b/meson_cross_files/windows-arm64.ini new file mode 100644 index 0000000..d4403ea --- /dev/null +++ b/meson_cross_files/windows-arm64.ini @@ -0,0 +1,5 @@ +[host_machine] +system = 'windows' +cpu_family = 'aarch64' +cpu = 'aarch64' +endian = 'little' diff --git a/noxfile.py b/noxfile.py index 401f6da..87168f0 100644 --- a/noxfile.py +++ b/noxfile.py @@ -11,6 +11,7 @@ DOCS_DIR = DIR / "docs" nox.options.sessions = ["lint"] +nox.options.verbose = True nox.options.default_venv_backend = "uv|virtualenv" @@ -34,12 +35,21 @@ def venv(session: nox.Session) -> None: session.run("hugo", "env", "--logLevel", "debug") +@nox.session(default=False, reuse_venv=True) +def editable(session: nox.Session) -> None: + """Smoke test console and module entry points from an editable install.""" + session.install("meson-python==0.19.0", "ziglang==0.15.2", "ninja") + session.install("--no-build-isolation", "-ve", ".") + session.run("python", "-m", "hugo", "version") + session.run("hugo", "version") + + def _get_version(session: nox.Session) -> str: - """Extract version from session posargs or setup.py.""" + """Extract version from session posargs or meson.build.""" if session.posargs: return session.posargs[0].lstrip("v") - content = (DIR / "setup.py").read_text() - match = re.search(r'HUGO_VERSION = "([0-9.]+)"', content) + content = (DIR / "meson.build").read_text() + match = re.search(r"version\s*:\s*'([0-9.]+)'", content) if not match: session.error("Could not determine version. Pass it as: nox -s tag -- 0.157.0") return match.group(1) diff --git a/pyproject.toml b/pyproject.toml index a359247..b49ac11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,10 @@ [build-system] requires = [ - "setuptools>=77.0.3", + "meson-python==0.19.0", "ziglang==0.15.2", ] -build-backend = "setuptools.build_meta" +build-backend = "hugo_meson_python_wrapper" +backend-path = ["scripts"] [project] name = "hugo" @@ -55,13 +56,12 @@ Homepage = "https://github.com/agriyakhetarpal/hugo-python-distributions" Issues = "https://github.com/agriyakhetarpal/hugo-python-distributions/issues" Changelog = "https://github.com/agriyakhetarpal/hugo-python-distributions/releases" -[tool.setuptools.packages.find] -where = ["src"] -include = ["hugo", "hugo.*"] - [project.scripts] hugo = "hugo.cli:__call" +[tool.meson-python] +allow-windows-internal-shared-libs = true + [tool.ruff] src = ["src"] lint.extend-select = [ diff --git a/scripts/build_hugo.py b/scripts/build_hugo.py new file mode 100644 index 0000000..aa8b295 --- /dev/null +++ b/scripts/build_hugo.py @@ -0,0 +1,280 @@ +""" +Build the Hugo binary from the vendored submodule. + +Invoked by meson.build's custom_target. All inputs come via CLI flags, so we +can also run this script standalone for debugging. +""" + +from __future__ import annotations + +import argparse +import os +import platform +import re +import shutil +import stat +import subprocess +import sys +from pathlib import Path + +HUGO_VENDOR_NAME = "hugo-python-distributions" + +HOST_GOOS = { + "darwin": "darwin", + "linux": "linux", + "win32": "windows", +}.get(sys.platform, sys.platform) + +HOST_GOARCH = { + "x86_64": "amd64", + "arm64": "arm64", + "AMD64": "amd64", + "aarch64": "arm64", + "x86": "386", + "i686": "386", + "i386": "386", + "s390x": "s390x", + "ppc64le": "ppc64le", + "armv7l": "arm", + "armv6l": "arm", + "riscv64": "riscv64", +}.get(platform.machine(), platform.machine()) + +ZIG_TARGET_MAP = { + ("darwin", "amd64"): "x86_64-macos-none", + ("darwin", "arm64"): "aarch64-macos-none", + ("linux", "amd64"): "x86_64-linux-gnu", + ("linux", "arm64"): "aarch64-linux-gnu", + ("linux", "arm"): "arm-linux-gnueabihf", + ("linux", "386"): "x86-linux-gnu", + ("linux", "ppc64le"): "powerpc64le-linux-gnu", + ("linux", "s390x"): "s390x-linux-gnu", + ("linux", "riscv64"): "riscv64-linux-gnu", + ("windows", "386"): "x86-windows-gnu", + ("windows", "amd64"): "x86_64-windows-gnu", + ("windows", "arm64"): "aarch64-windows-gnu", +} + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser() + p.add_argument("--hugo-src", required=True, type=Path) + p.add_argument("--cache", required=True, type=Path) + p.add_argument("--version", required=True) + p.add_argument("--output", required=True, type=Path) + p.add_argument("--use-zig", default="false") + p.add_argument("--goos", default="") + p.add_argument("--goarch", default="") + return p.parse_args() + + +def check_dependencies(use_zig: bool) -> None: + try: + subprocess.check_call( + ["go", "version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + except OSError as err: + msg = "Go toolchain not found. Please install Go from https://go.dev/dl/ or your package manager." + raise OSError(msg) from err + + try: + subprocess.check_call( + ["git", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + except OSError as err: + msg = "Git not found. Please install Git from https://git-scm.com/downloads or your package manager." + raise OSError(msg) from err + + if use_zig: + try: + subprocess.check_call( + [sys.executable, "-m", "ziglang", "version"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except OSError as err: + msg = "Zig compiler not found. Please install Zig from https://ziglang.org/download/, from PyPI as ziglang, or your package manager." + raise OSError(msg) from err + return + + for cc in ("gcc", "clang"): + try: + subprocess.check_call( + [cc, "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + return + except OSError: + continue + msg = "GCC/Clang not found. Please install GCC or Clang via your package manager." + raise OSError(msg) + + +def setup_zig_compiler(goos: str, goarch: str) -> None: + target = ZIG_TARGET_MAP.get((goos, goarch)) + if not target: + print( + f"Warning: there is no Zig target combination for GOOS={goos} and GOARCH={goarch}", + file=sys.stderr, + ) + return + cc = f"{sys.executable} -m ziglang cc -target {target}" + cxx = f"{sys.executable} -m ziglang c++ -target {target}" + if target == "x86-windows-gnu": + cc += " -w" + cxx += " -w" + os.environ["CC"] = cc + os.environ["CXX"] = cxx + os.environ["CGO_CFLAGS"] = "-g0 -O3 -ffunction-sections -fdata-sections" + os.environ["CGO_LDFLAGS"] = "-s -w -Wl,--gc-sections" + + +class SubmoduleVcsSwap: + """Replace the submodule .git file with a .git directory such that Go's + VCS stamping reads the Hugo submodule's HEAD, not that of the parent + repository. + + Submodules store a `.git` file that points into the parent repository's + `.git/modules` directory. Go's VCS detection follows that link into the + parent repo and produces wrong metadata. We copy the actual git directory + into the submodule worktree temporarily and rewrite the worktree config + entry. This is restored on exit. + """ + + def __init__(self, hugo_src: Path) -> None: + self.hugo_git = hugo_src / ".git" + self._saved: str | None = None + + def __enter__(self) -> SubmoduleVcsSwap: + if not self.hugo_git.is_file(): + return self + content = self.hugo_git.read_text() + self._saved = content + gitdir = content.strip().split("gitdir: ", 1)[1] + gitdir_abs = (self.hugo_git.parent / gitdir).resolve() + self.hugo_git.unlink() + shutil.copytree(str(gitdir_abs), str(self.hugo_git)) + if sys.platform == "win32": + for p in self.hugo_git.rglob("*"): + if p.is_file(): + p.chmod(p.stat().st_mode | stat.S_IWRITE) + config_file = self.hugo_git / "config" + if config_file.exists(): + cfg = config_file.read_text() + cfg = re.sub(r"worktree\s*=\s*[^\n]+", "worktree = ..", cfg) + config_file.write_text(cfg) + return self + + def __exit__(self, *exc: object) -> None: + if self._saved is None: + return + if self.hugo_git.is_dir() and not self.hugo_git.is_symlink(): + shutil.rmtree(self.hugo_git) + elif self.hugo_git.exists() or self.hugo_git.is_symlink(): + self.hugo_git.unlink() + self.hugo_git.write_text(self._saved) + + +def get_hugo_commit_date(hugo_src: Path) -> str: + """Return the Hugo submodule's HEAD commit date (ISO 8601). + + Falls back to a stamp file (`/.hugo_commit_date`) when + `.git` is absent, which is the case for sdist builds. + """ + stamp = hugo_src / ".hugo_commit_date" + try: + date = subprocess.check_output( + ["git", "log", "-1", "--format=%cI"], + cwd=hugo_src, + text=True, + stderr=subprocess.DEVNULL, + ).strip() + if date: + stamp.write_text(date) + return date + except (subprocess.CalledProcessError, OSError): + pass + if stamp.exists(): + return stamp.read_text().strip() + return "" + + +def locate_built_binary(gopath: Path, goos: str, goarch: str, exe_ext: str) -> Path: + """Find the binary that ``go install`` produced. + + Go places it at ``$GOPATH/bin/hugo`` for native builds, or + ``$GOPATH/bin/$GOOS_$GOARCH/hugo`` when cross-compiling. + """ + cross = goos != HOST_GOOS or goarch != HOST_GOARCH + if cross: + candidate = gopath / "bin" / f"{goos}_{goarch}" / ("hugo" + exe_ext) + if candidate.exists(): + return candidate + return gopath / "bin" / ("hugo" + exe_ext) + + +def main() -> int: + args = parse_args() + use_zig = args.use_zig.lower() == "true" + goos = args.goos or HOST_GOOS + goarch = args.goarch or HOST_GOARCH + cache = args.cache.resolve() + hugo_src = args.hugo_src.resolve() + output = args.output.resolve() + + cache.mkdir(parents=True, exist_ok=True) + shutil.rmtree(cache / "bin", ignore_errors=True) + + check_dependencies(use_zig) + + os.environ["CGO_ENABLED"] = "1" + os.environ["GO111MODULE"] = "on" + os.environ["GOPATH"] = str(cache) + os.environ["GOCACHE"] = str(cache) + os.environ["GOOS"] = goos + os.environ["GOARCH"] = goarch + + if goarch == "arm" and goos == "linux": + default_goarm = "6" if platform.machine() == "armv6l" else "7" + os.environ.setdefault("GOARM", default_goarm) + + if use_zig: + setup_zig_compiler(goos, goarch) + + ldflags = [ + f"-s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo={HUGO_VENDOR_NAME}", + ] + commit_date = get_hugo_commit_date(hugo_src) + if commit_date: + ldflags.append( + f"-X github.com/gohugoio/hugo/common/hugo.buildDate={commit_date}", + ) + if goos == "windows": + ldflags.append("-extldflags '-static'") + + with SubmoduleVcsSwap(hugo_src): + subprocess.check_call( + [ + "go", + "install", + "-trimpath", + "-v", + "-ldflags", + " ".join(ldflags), + "-tags", + "extended,withdeploy", + ], + cwd=hugo_src, + ) + + exe_ext = ".exe" if goos == "windows" else "" + built = locate_built_binary(cache, goos, goarch, exe_ext) + output.parent.mkdir(parents=True, exist_ok=True) + if output.exists(): + output.unlink() + shutil.copy2(built, output) + output.chmod(output.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/generate_meson_cross_file.py b/scripts/generate_meson_cross_file.py new file mode 100644 index 0000000..bbbdd61 --- /dev/null +++ b/scripts/generate_meson_cross_file.py @@ -0,0 +1,62 @@ +""" +Emits a Meson cross file for a given GOOS and GOARCH combination, to derive +the wheel platform tag from `[host_machine]` for cross-compilation builds. +These are used for Zig-based cross-compilation in CI, but can also be used +for any general cross-compiler (say, Clang, or a custom-built GCC toolchain). + +Usage: + python scripts/generate_meson_cross_file.py --goos OS --goarch ARCH --output PATH.txt +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +# This is (goos, goarch), mapping to (system, cpu_family, cpu, endian) +HOST_MACHINE_MAP: dict[tuple[str, str], tuple[str, str, str, str]] = { + ("linux", "amd64"): ("linux", "x86_64", "x86_64", "little"), + ("linux", "arm64"): ("linux", "aarch64", "aarch64", "little"), + ("linux", "arm"): ("linux", "arm", "armv7l", "little"), + ("linux", "386"): ("linux", "x86", "i686", "little"), + ("linux", "ppc64le"): ("linux", "ppc64", "ppc64le", "little"), + ("linux", "s390x"): ("linux", "s390x", "s390x", "big"), + ("linux", "riscv64"): ("linux", "riscv64", "riscv64", "little"), + ("windows", "amd64"): ("windows", "x86_64", "x86_64", "little"), + ("windows", "arm64"): ("windows", "aarch64", "aarch64", "little"), + ("windows", "386"): ("windows", "x86", "i686", "little"), + ("darwin", "amd64"): ("darwin", "x86_64", "x86_64", "little"), + ("darwin", "arm64"): ("darwin", "aarch64", "aarch64", "little"), +} + + +def render(goos: str, goarch: str) -> str: + key = (goos, goarch) + if key not in HOST_MACHINE_MAP: + sys.exit(f"This is an unsupported GOOS/GOARCH combination: {goos}/{goarch}") + + system, family, cpu, endian = HOST_MACHINE_MAP[key] + return ( + "[host_machine]\n" + f"system = '{system}'\n" + f"cpu_family = '{family}'\n" + f"cpu = '{cpu}'\n" + f"endian = '{endian}'\n" + ) + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--goos", required=True) + p.add_argument("--goarch", required=True) + p.add_argument("--output", required=True, type=Path) + args = p.parse_args() + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(render(args.goos, args.goarch)) + print(f"Wrote {args.output}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/hugo_meson_python_wrapper.py b/scripts/hugo_meson_python_wrapper.py new file mode 100644 index 0000000..f3011af --- /dev/null +++ b/scripts/hugo_meson_python_wrapper.py @@ -0,0 +1,142 @@ +""" +This is a thin PEP 517 wrapper around meson-python's PEP 517 support. +Its primary purpose is to set the _PYTHON_HOST_PLATFORM env var for +cross-compilation without requiring users to set it by hand, based on +requirements passed via the --cross-file=... flag in config_settings. + +It looks for `--cross-file=...` in the wheel build's `config_settings` +(`-Csetup-args=--cross-file=foo.ini`), parses the cross file, and sets +the `_PYTHON_HOST_PLATFORM` environment variable, so that meson-python +will tag the wheel for the cross without needing users to set it by hand. + +Native builds (without a cross file) are no-ops here, i.e., this wrapper +does nothing in that case and just passes through to meson-python. Cross +builds with an unsupported (host_machine.system, host_machine.cpu_family) +tuple will likely produce an incorrectly tagged wheel that will need to +be manually renamed. +""" + +from __future__ import annotations + +import configparser +import os +import re +from pathlib import Path +from typing import Any + +import mesonpy + +# (host_machine.system, host_machine.cpu_family) -> _PYTHON_HOST_PLATFORM +PLATFORM_TAGS_MAP: dict[tuple[str, str], str] = { + ("linux", "x86_64"): "linux_x86_64", + ("linux", "aarch64"): "linux_aarch64", + ("linux", "arm"): "linux_armv7l", + ("linux", "x86"): "linux_i686", + ("linux", "ppc64"): "linux_ppc64le", + ("linux", "s390x"): "linux_s390x", + ("linux", "riscv64"): "linux_riscv64", + ("windows", "x86_64"): "win_amd64", + ("windows", "aarch64"): "win_arm64", + ("windows", "x86"): "win32", + ( + "darwin", + "x86_64", + ): "macosx_10_13_x86_64", # TODO: figure out what to do about MACOSX_DEPLOYMENT_TARGET + ( + "darwin", + "aarch64", + ): "macosx_11_0_arm64", # TODO: figure out what to do about MACOSX_DEPLOYMENT_TARGET +} + + +def _flatten(value: Any) -> list[str]: + if value is None: + return [] + if isinstance(value, str): + return [value] + return list(value) + + +def _strip_quotes(string: str) -> str: + return string.strip().strip("'\"") + + +def _host_platform_tag(config_settings: dict[str, Any] | None) -> str | None: + """Return the target wheel platform tag from a Meson cross file, if any.""" + if not config_settings: + return None + setup_args = _flatten(config_settings.get("setup-args")) + cross_file: Path | None = None + for arg in setup_args: + m = re.match(r"--cross-file[= ](.+)$", arg) + if m: + cross_file = Path(m.group(1)) + break + if cross_file is None or not cross_file.exists(): + return None + + cfg = configparser.ConfigParser() + cfg.read(cross_file) + if "host_machine" not in cfg: + return None + + system = _strip_quotes(cfg["host_machine"].get("system", "")) + family = _strip_quotes(cfg["host_machine"].get("cpu_family", "")) + return PLATFORM_TAGS_MAP.get((system, family)) + + +def _maybe_set_host_platform(config_settings: dict[str, Any] | None) -> None: + """If config_settings contains a Meson cross file, force meson-python's wheel tag.""" + tag = _host_platform_tag(config_settings) + if tag is None: + return + + os.environ["_PYTHON_HOST_PLATFORM"] = tag + mesonpy._tags.get_platform_tag = lambda: ( + tag + ) # TODO: drop this hack/use of private API + + +# Unchanged meson-python PEP 517 entry points below + + +def build_wheel( + wheel_directory: str, + config_settings: dict[str, Any] | None = None, + metadata_directory: str | None = None, +) -> str: + _maybe_set_host_platform(config_settings) + return mesonpy.build_wheel(wheel_directory, config_settings, metadata_directory) + + +def build_editable( + wheel_directory: str, + config_settings: dict[str, Any] | None = None, + metadata_directory: str | None = None, +) -> str: + _maybe_set_host_platform(config_settings) + return mesonpy.build_editable(wheel_directory, config_settings, metadata_directory) + + +def build_sdist( + sdist_directory: str, config_settings: dict[str, Any] | None = None +) -> str: + return mesonpy.build_sdist(sdist_directory, config_settings) + + +def get_requires_for_build_wheel( + config_settings: dict[str, Any] | None = None, +) -> list[str]: + return mesonpy.get_requires_for_build_wheel(config_settings) + + +def get_requires_for_build_editable( + config_settings: dict[str, Any] | None = None, +) -> list[str]: + return mesonpy.get_requires_for_build_editable(config_settings) + + +def get_requires_for_build_sdist( + config_settings: dict[str, Any] | None = None, +) -> list[str]: + return mesonpy.get_requires_for_build_sdist(config_settings) diff --git a/scripts/prune_sdist.py b/scripts/prune_sdist.py new file mode 100644 index 0000000..e44ebe3 --- /dev/null +++ b/scripts/prune_sdist.py @@ -0,0 +1,44 @@ +"""Prunes a lot of large/irrelevant trees from the sdist, mainly from the +Hugo submodule, which is not needed for users building from sdist and would +just artificially inflate the distributions' size. This is invoked by the +`meson dist` command. + +TODO: figure out a better, more declarative way to do this, because I can't +find a meson-python sdist include/exclude mechanism. There's only one for +wheels right now. +""" + +from __future__ import annotations + +import os +import shutil +import sys +from pathlib import Path + +EXCLUDED_DIRS = ( + "hugo-src/docs", + "hugo-src/testscripts", + "docs", + "src/hugo/binaries", + "hugo_cache", + "build", +) + + +def main() -> int: + root = Path(os.environ["MESON_DIST_ROOT"]) + for rel in EXCLUDED_DIRS: + target = root / rel + if target.exists(): + shutil.rmtree(target) + print(f"pruned {rel}") + + for testdata in root.glob("hugo-src/**/testdata"): + if testdata.is_dir(): + shutil.rmtree(testdata) + print(f"pruned {testdata.relative_to(root)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/setup.py b/setup.py deleted file mode 100644 index a74f1c4..0000000 --- a/setup.py +++ /dev/null @@ -1,646 +0,0 @@ -import os -import platform -import re -import shutil -import stat -import subprocess -import sys -from pathlib import Path - -from setuptools import Command, Extension, setup -from setuptools.command.bdist_wheel import bdist_wheel -from setuptools.command.build_ext import build_ext -from setuptools.command.build_py import build_py - -# ------ Hugo build configuration and constants ------------------------------------ - -# Also update src/hugo/cli.py -HUGO_VERSION = "0.160.1" - -# The Go toolchain will use the hugo_cache/ directory for GOPATH and GOCACHE. -# We will point the build command to that location to build Hugo from source -HUGO_CACHE_DIR = "hugo_cache" -# Path to the Hugo source submodule -HUGO_SRC_DIR = "hugo" -FILE_EXT = ( - ".exe" if (sys.platform == "win32" or os.environ.get("GOOS") == "windows") else "" -) -# The vendor name is used to set the vendorInfo variable in the Hugo binary -HUGO_VENDOR_NAME = "hugo-python-distributions" - -# Normalise platform strings to match the Go toolchain -HUGO_PLATFORM = { - "darwin": "darwin", - "linux": "linux", - "win32": "windows", -}[sys.platform] - -# Normalise architecture strings to match the Go toolchain -HUGO_ARCH = { - "x86_64": "amd64", - "arm64": "arm64", - "AMD64": "amd64", - "aarch64": "arm64", - "x86": "386", - "i686": "386", - "i386": "386", - "s390x": "s390x", - "ppc64le": "ppc64le", - "armv7l": "arm", - "armv6l": "arm", - "riscv64": "riscv64", -}[platform.machine()] - -# Name of the Hugo binary that will be built -HUGO_BINARY_NAME = ( - f"hugo-{HUGO_VERSION}-{os.environ.get('GOOS', HUGO_PLATFORM)}-{os.environ.get('GOARCH', HUGO_ARCH)}" - + FILE_EXT -) - -# Write a Hugo commit date stamp file, such that it is available for both wheel builds -# (via _get_hugo_commit_date) and sdist builds (included via MANIFEST.in). This runs -# at setup.py parse time, before setuptools collects files for the sdist. -_hugo_stamp = Path(HUGO_SRC_DIR).resolve() / ".hugo_commit_date" -try: - _commit_date = subprocess.check_output( - ["git", "log", "-1", "--format=%cI"], - cwd=Path(HUGO_SRC_DIR).resolve(), - text=True, - stderr=subprocess.DEVNULL, - ).strip() - if _commit_date: - _hugo_stamp.write_text(_commit_date) -except (subprocess.CalledProcessError, OSError): - pass - -# ---------------------------------------------------------------------------------- - - -class HugoWriter(build_py): - """ - A custom pre-installation command that writes the version of Hugo being built - directly to src/hugo/cli.py so that the version is available at runtime. - """ - - def initialize_options(self) -> None: - return super().initialize_options() - - def finalize_options(self) -> None: - return super().finalize_options() - - def run(self) -> None: - cli_file_path = Path(__file__).parent / "src" / "hugo" / "cli.py" - content = cli_file_path.read_text() - version = re.sub( - r'HUGO_VERSION = "[0-9.]+"', f'HUGO_VERSION = "{HUGO_VERSION}"', content - ) - with open(cli_file_path, "w") as cli_file: # noqa: PTH123 - cli_file.write(version) - - super().run() - - -class HugoBuilder(build_ext): - """ - Custom extension command that builds Hugo from source, placing the binary into - the package directory for further use. - """ - - def initialize_options(self): - super().initialize_options() - self.hugo_version = None - self.hugo_platform = None - self.hugo_arch = None - - def finalize_options(self): - # Platforms and architectures that we will build Hugo natively for are: - # i.e., a subset of "go tool dist list": - # 1. darwin/amd64 - # 2. darwin/arm64 - # 3. linux/amd64 - # 4. linux/arm64 - # 5. windows/amd64 - # The platform is the first part of the string, the architecture is the second. - # We will mangle the hugo binary name to include the platform and architecture - # so that we can build Hugo for multiple platforms and architectures. - # The platform is used to set the GOOS environment variable, the architecture - # is used to set the GOARCH environment variable, and they must be exactly these - # strings for the Go toolchain to work. - # Note to self: go tool dist list -json | jq -r "map(select(.CgoSupported)) | .[] | .GOOS + \"/\" + .GOARCH" - super().finalize_options() - self.hugo_version = HUGO_VERSION - self.hugo_platform = HUGO_PLATFORM - self.hugo_arch = HUGO_ARCH - - def run(self): - """ - Build Hugo from source and place the binary in the package directory, mangling - # the name so that it is unique to the version of Hugo being built. - """ - - # If Hugo cache does not exist, create it - if not Path(HUGO_CACHE_DIR).exists(): - Path(HUGO_CACHE_DIR).mkdir(parents=True) - - # The binary is put into GOBIN, which is set to the package directory - # (src/hugo/binaries/) for use in editable mode. The binary is copied - # into the wheel afterwards - # Error: GOBIN cannot be set if GOPATH is set when compiling for different - # architectures, so we use the default GOPATH/bin as the place to copy - # binaries from - # os.environ["GOBIN"] = os.path.join( - # os.path.dirname(os.path.abspath(__file__)), "src", "hugo", "binaries" - # ) - os.environ["CGO_ENABLED"] = "1" - os.environ["GO111MODULE"] = "on" - os.environ["GOPATH"] = str(Path(HUGO_CACHE_DIR).resolve()) - # it must be absolute (Go requirement) - - # Set GOCACHE to the hugo_cache/ directory so that the Go toolchain - # caches the build artifacts there for future use. - os.environ["GOCACHE"] = str(Path(HUGO_CACHE_DIR).resolve()) - - os.environ["GOOS"] = os.environ.get("GOOS", self.hugo_platform) - os.environ["GOARCH"] = os.environ.get("GOARCH", self.hugo_arch) - # i.e., allow override if GOARCH is set! - - if os.environ.get("GOARCH") == "arm" and os.environ.get("GOOS") == "linux": - default_goarm = "6" if platform.machine() == "armv6l" else "7" - os.environ["GOARM"] = os.environ.get("GOARM", default_goarm) - - # New: Setup Zig compiler if USE_ZIG is set - if os.environ.get("USE_ZIG"): - self._setup_zig_compiler() - - # Build Hugo from source using the Go toolchain, place it into GOBIN - # Requires the following dependencies: - # - # 1. Go - # 2. GCC/Clang - # 3. Git - # - # Once built the files are cached into GOPATH for future use - - # Delete hugo_cache/bin/ + files inside, if left over from a previous build - shutil.rmtree(Path(HUGO_CACHE_DIR).resolve() / "bin", ignore_errors=True) - - # Check for compilers, toolchains, etc. and raise helpful errors if they - # are not found. These are essentially smoke tests to ensure that the - # build environment is set up correctly. - self._check_build_dependencies() - - # These ldflags are passed to the Go linker to set variables at runtime. - # The buildDate ldflag is a fallback for sdist builds where .git is absent - # and Go cannot embed VCS info. In normal builds (git checkout / submodule), - # _prepare_submodule_vcs ensures Go's VCS stamping reads the correct - # commit hash and date from the Hugo submodule directly. - commit_date = self._get_hugo_commit_date() - - ldflags = [ - f"-s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo={HUGO_VENDOR_NAME}" - ] - if commit_date: - ldflags.append( - f"-X github.com/gohugoio/hugo/common/hugo.buildDate={commit_date}" - ) - - # Build a static binary on Windows to avoid missing DLLs from MinGW, - # i.e., libgcc_s_seh-1.dll, libstdc++-6.dll, etc. - BUILDING_FOR_WINDOWS = ( - os.environ.get("GOOS") == "windows" or sys.platform == "win32" - ) - - if BUILDING_FOR_WINDOWS: - ldflags.append("-extldflags '-static'") - - # Temporarily convert the submodule's .git file into a symlink to the - # real git directory so that Go's VCS stamping embeds the correct - # commit hash and date from the Hugo submodule (not the parent repo). - self._prepare_submodule_vcs() - try: - self._build_hugo(ldflags) - finally: - self._restore_submodule_vcs() - - self._rename_and_move_binary() - - @staticmethod - def _get_hugo_commit_date(): - """Get the commit date from the Hugo submodule's git history. - - Falls back to a stamp file (hugo/.hugo_commit_date) for sdist builds - where .git is absent. Writes the stamp file when git is available so - that it is included in the sdist. - """ - hugo_src_dir = Path(HUGO_SRC_DIR).resolve() - stamp_file = hugo_src_dir / ".hugo_commit_date" - - # Try git first (works in git checkout and submodule) - try: - commit_date = subprocess.check_output( - ["git", "log", "-1", "--format=%cI"], - cwd=hugo_src_dir, - text=True, - stderr=subprocess.DEVNULL, - ).strip() - if commit_date: - # Write stamp file so sdist builds can use it - stamp_file.write_text(commit_date) - return commit_date - except (subprocess.CalledProcessError, OSError): - pass - - # Fall back to stamp file (sdist builds) - if stamp_file.exists(): - return stamp_file.read_text().strip() - - return "" - - def _prepare_submodule_vcs(self): - """Replace the submodule .git file with a real .git directory so Go's - VCS stamping embeds the correct commit hash and date. - - Submodules store a .git *file* (e.g. ``gitdir: ../.git/modules/hugo``) - instead of a .git directory. Go's VCS detection follows this into the - parent repo, producing wrong metadata. We copy the actual git directory - into ``hugo/.git/`` and rewrite the ``worktree`` config entry so that - ``git status`` works correctly during the build. Works on all platforms - (no symlinks required). - """ - hugo_git = Path(HUGO_SRC_DIR).resolve() / ".git" - self._saved_git_file = None - - if hugo_git.is_file(): - content = hugo_git.read_text() - self._saved_git_file = content - # e.g. "gitdir: ../.git/modules/hugo\n" - gitdir = content.strip().split("gitdir: ", 1)[1] - gitdir_abs = (hugo_git.parent / gitdir).resolve() - hugo_git.unlink() - # Copy the real git directory into hugo/.git/. Git pack files - # are read-only on Windows, so we must make them writable after - # copying, otherwise both the Go build and later cleanup fail - # with [WinError 5] Access is denied. - shutil.copytree(str(gitdir_abs), str(hugo_git)) - if sys.platform == "win32": - for p in hugo_git.rglob("*"): - if p.is_file(): - p.chmod(p.stat().st_mode | stat.S_IWRITE) - # Fix up the worktree path in the config — it was relative to - # .git/modules/hugo/ and now needs to point to the parent of - # hugo/.git/ (i.e. hugo/ itself). - config_file = hugo_git / "config" - if config_file.exists(): - cfg = config_file.read_text() - cfg = re.sub(r"worktree\s*=\s*[^\n]+", "worktree = ..", cfg) - config_file.write_text(cfg) - - def _restore_submodule_vcs(self): - """Restore the original submodule .git file after building.""" - hugo_git = Path(HUGO_SRC_DIR).resolve() / ".git" - if self._saved_git_file is not None: - if hugo_git.is_dir() and not hugo_git.is_symlink(): - shutil.rmtree(hugo_git) - elif hugo_git.exists() or hugo_git.is_symlink(): - hugo_git.unlink() - hugo_git.write_text(self._saved_git_file) - - def _setup_zig_compiler(self): - goos = os.environ.get("GOOS", self.hugo_platform) - goarch = os.environ.get("GOARCH", self.hugo_arch) - - zig_target_map = { - ("darwin", "amd64"): "x86_64-macos-none", - ("darwin", "arm64"): "aarch64-macos-none", - ("linux", "amd64"): "x86_64-linux-gnu", - ("linux", "arm64"): "aarch64-linux-gnu", - ("linux", "arm"): "arm-linux-gnueabihf", - ("linux", "386"): "x86-linux-gnu", - ("linux", "ppc64le"): "powerpc64le-linux-gnu", - ("linux", "s390x"): "s390x-linux-gnu", - ("linux", "riscv64"): "riscv64-linux-gnu", - ("windows", "386"): "x86-windows-gnu", - ("windows", "amd64"): "x86_64-windows-gnu", - ("windows", "arm64"): "aarch64-windows-gnu", - } - - zig_target = zig_target_map.get((goos, goarch)) - if zig_target: - os.environ["CC"] = f"{sys.executable} -m ziglang cc -target {zig_target}" - os.environ["CXX"] = f"{sys.executable} -m ziglang c++ -target {zig_target}" - if zig_target == "x86-windows-gnu": - os.environ["CC"] += " -w" - os.environ["CXX"] += " -w" - # Add additional flags to the linker to ensure that the binary is - # stripped of debug information and is as small as possible for release - os.environ["CGO_CFLAGS"] = "-g0 -O3 -ffunction-sections -fdata-sections" - os.environ["CGO_LDFLAGS"] = "-s -w -Wl,--gc-sections" - else: - print(f"Warning: No Zig target found for GOOS={goos} and GOARCH={goarch}") - - def _check_build_dependencies(self): - # Go toolchain is required for building Hugo - try: - subprocess.check_call( - ["go", "version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) - except OSError as err: - error_message = "Go toolchain not found. Please install Go from https://go.dev/dl/ or your package manager." - raise OSError(error_message) from err - - # Check for Zig compiler only if USE_ZIG is set - if os.environ.get("USE_ZIG"): - try: - subprocess.check_call( - [sys.executable, "-m", "ziglang", "version"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - except OSError as err: - error_message = "Zig compiler not found. Please install Zig from https://ziglang.org/download/ or your package manager." - raise OSError(error_message) from err - else: - # GCC/Clang is required for building Hugo because CGO is enabled - try: - subprocess.check_call( - ["gcc", "--version"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - except OSError: - try: - subprocess.check_call( - ["clang", "--version"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - except OSError as err: - error_message = "GCC/Clang not found. Please install GCC or Clang via your package manager." - raise OSError(error_message) from err - - # Git is required for building Hugo to fetch dependencies from various Git repositories - try: - subprocess.check_call( - ["git", "--version"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - except OSError as err: - error_message = "Git not found. Please install Git from https://git-scm.com/downloads or your package manager." - raise OSError(error_message) from err - - def _build_hugo(self, ldflags): - subprocess.check_call( - [ - "go", - "install", - "-trimpath", - "-v", - "-ldflags", - " ".join(ldflags), - "-tags", - "extended,withdeploy", - ], - cwd=Path(HUGO_SRC_DIR).resolve(), - ) - # TODO: introduce some error handling here to detect compilers, etc. - - def _rename_and_move_binary(self): - # Mangle the name of the compiled executable to include the version, the - # platform, and the architecture of Hugo being built. - # The binary is present in GOPATH (i.e, either at hugo_cache/bin/ or at - # hugo_cache/bin/$GOOS_$GOARCH/bin) and now GOBIN is not set, so we need - # to copy it from there. - - # If the GOARCH is not the same as self.hugo_arch, we are cross-compiling, so - # we need to go into the GOOS_GOARCH/bin folder to find the binary rather than - # the GOPATH/bin folder. - - # four scenarios: - # 1 cross compiling: GOARCH != self.hugo_arch - # 2 cross compiling: GOOS != self.hugo_platform - # 3 cross compiling: GOARCH != self.hugo_arch and GOOS != self.hugo_platform - # 4 not cross compiling: GOARCH == self.hugo_arch and GOOS == self.hugo_platform - - # scenario 3, it checks for both GOARCH and GOOS, and if both are different - if (os.environ.get("GOARCH") != self.hugo_arch) and ( - os.environ.get("GOOS") != self.hugo_platform - ): - original_name = ( - Path(os.environ.get("GOPATH")) - / "bin" - / f"{os.environ.get('GOOS')}_{os.environ.get('GOARCH')}" - / ("hugo" + FILE_EXT) - ) - # scenario 1, here GOARCH is different - elif os.environ.get("GOARCH") != self.hugo_arch: - original_name = ( - Path(os.environ.get("GOPATH")) - / "bin" - / (f"{self.hugo_platform}_{os.environ.get('GOARCH')}") - / ("hugo" + FILE_EXT) - ) - # scenario 2, here GOOS is different - elif os.environ.get("GOOS") != self.hugo_platform: - original_name = ( - Path(os.environ.get("GOPATH")) - / "bin" - / (f"{os.environ.get('GOOS')}_{self.hugo_arch}") - / ("hugo" + FILE_EXT) - ) - # scenario 4, here GOARCH and GOOS both are the same - else: - original_name = Path(os.environ.get("GOPATH")) / "bin" / ("hugo" + FILE_EXT) - - new_name = ( - Path(os.environ.get("GOPATH")) - / "bin" - / ( - f"hugo-{HUGO_VERSION}-{os.environ.get('GOOS', self.hugo_platform)}-{os.environ.get('GOARCH', self.hugo_arch)}" - + FILE_EXT - ) - ) - original_name.rename(new_name) - - # Copy the new_name file into a folder binaries/ inside hugo/ - # so that it is included in the wheel. - # basically we are copying hugo-HUGO_VERSION-PLATFORM-ARCH into - # src/hugo/binaries/ and creating the folder if it does not exist. - - binaries_dir = Path(__file__).parent / "src" / "hugo" / "binaries" - if not binaries_dir.exists(): - binaries_dir.mkdir() - - # if the binary already exists, delete it, and then copy the new binary - # to ensure that the binary is always the newest rendition - new_binary_path = binaries_dir / new_name.name - if new_binary_path.exists(): - new_binary_path.unlink() - new_name.rename(new_binary_path) - - -# https://github.com/pypa/setuptools/issues/1347: setuptools does not support -# the clean command from distutils yet. so we need to use a workaround that gets -# called inside bdist_wheel invocation. -class Cleaner(Command): - """ - Custom command that cleans the build directory of the package at the project root. - """ - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - """Clean ancillary files at runtime.""" - - here = os.path.normpath(Path(__file__).parent.resolve()) - files_to_clean = ["./build", "./*.pyc", "./*.egg-info", "./__pycache__"] - - for path_spec in files_to_clean: - # Make paths absolute and relative to this path - abs_paths = Path(here).glob(path_spec) - for path in [str(p) for p in abs_paths]: - if not path.startswith(str(here)): - # raise error if path in files_to_clean is absolute + outside - # this directory - msg = f"{path} is not a path around {here}" - raise ValueError(msg) - shutil.rmtree(path) - - -# Mock setuptools into thinking that we are building a target binary on a host machine -# so that the wheel gets tagged correctly when building or cross-compiling. -class HugoWheel(bdist_wheel): - """ - A customised wheel build command that sets the platform tags to accommodate - the varieties of the GOARCH and GOOS environment variables when cross-compiling - the Hugo binary with any available cross-compilation toolchain. - """ - - def initialize_options(self): - super().initialize_options() - - def finalize_options(self): - super().finalize_options() - - def get_tag(self): - python_tag, abi_tag, platform_tag = bdist_wheel.get_tag(self) - # Build for all Python versions and set ABI tag to "none" because - # the Hugo binary is not a CPython extension, it is a self-contained - # non-Pythonic binary. - python_tag, abi_tag = "py3", "none" - - # Handle platform tags during cross-compilation from one platform to another - # ========================================================================== - # Here we will check for the GOOS environment variable and if it doesn't match - # HUGO_PLATFORM: if it doesn't match, we are cross-compiling, so we need to set - # the platform tag to the correct value. - # - # We have the following scenarios: - # - # 1. Cross-compiling from macOS arm64/x86_64 to Linux arm64/x86_64 - # 2. Cross-compiling from macOS arm64/x86_64 to Windows arm64/x86_64/x86 - # 3. Cross-compiling from Linux arm64/x86_64 to macOS arm64/x86_64 - # 4. Cross-compiling from Linux arm64/x86_64 to Windows arm64/x86_64/x86 - # 5. Cross-compiling from Windows arm64/x86_64/x86 to macOS arm64/x86_64 - # 6. Cross-compiling from Windows arm64/x86_64/x86 to Linux arm64/x86_64 - # - # These checks will be activated only when GOOS is set. If GOOS is not set, - # we will use the platform tag as is based on the above checks. - - # Handle cross-compilation on Linux via the Zig compiler - # ====================================================== - if os.environ.get("GOOS") == "linux": - if os.environ.get("GOARCH") == "arm64": - platform_tag = "linux_aarch64" - elif os.environ.get("GOARCH") == "amd64": - platform_tag = "linux_x86_64" - elif os.environ.get("GOARCH") == "ppc64le": - platform_tag = "linux_ppc64le" - elif os.environ.get("GOARCH") == "s390x": - platform_tag = "linux_s390x" - elif os.environ.get("GOARCH") == "arm": - platform_tag = "linux_armv7l" - elif os.environ.get("GOARCH") == "386": - platform_tag = "linux_i686" - elif os.environ.get("GOARCH") == "riscv64": - platform_tag = "linux_riscv64" - - # Handle cross-compilation on/to Windows via the Zig compiler - # =========================================================== - elif os.environ.get("GOOS") == "windows": - if os.environ.get("GOARCH") == "arm64": - platform_tag = "win_arm64" - elif os.environ.get("GOARCH") == "amd64": - platform_tag = "win_amd64" - elif os.environ.get("GOARCH") == "386": - platform_tag = "win32" - - # Cross-compiling to macOS or on macOS via the Zig or Xcode toolchains - # ==================================================================== - # Also, ensure correct platform tags for macOS arm64 and macOS x86_64 - # since macOS 3.12 Python GH Actions runners are mislabelling the platform - # tag to be universal2, see: https://github.com/pypa/wheel/issues/573 - # Also, let cibuildwheel handle the platform tags if it is being used, - # since that is where we won't cross-compile at all but use the native - # GitHub Actions runners. - elif (os.environ.get("GOOS") == "darwin") and ( - os.environ.get("CIBUILDWHEEL") != "1" - ): - if os.environ.get("GOARCH") == "arm64": - platform_tag = "macosx_11_0_arm64" - elif os.environ.get("GOARCH") == "amd64": - platform_tag = "macosx_10_13_x86_64" - - return python_tag, abi_tag, platform_tag - - def run(self): - self.root_is_pure = False # ensure that the wheel is tagged as a binary wheel - - self.run_command("clean") # clean the build directory before building the wheel - - # ensure that the binary is copied into the binaries/ folder and then - # into the wheel. - hugo_binary = ( - Path(__file__).parent - / "src" - / "hugo" - / "binaries" - / f"hugo-{HUGO_VERSION}-{os.environ.get('GOOS', HUGO_PLATFORM)}-{os.environ.get('GOARCH', HUGO_ARCH)}{FILE_EXT}" - ) - - # if the binary does not exist, then we need to build it, so invoke - # the build_ext command again and proceed to build the binary - if not Path(hugo_binary).exists(): - self.run_command("build_ext") - - # now that the binary exists, we have ensured its presence in the wheel - super().run() - - -setup( - ext_modules=[ - Extension( - name="hugo.build", - sources=[ - f"src/hugo/binaries/{HUGO_BINARY_NAME}", - ], - ) - ], - cmdclass={ - "build_py": HugoWriter, - "build_ext": HugoBuilder, - "clean": Cleaner, - "bdist_wheel": HugoWheel, - }, - package_data={ - "hugo": [ - f"binaries/{HUGO_BINARY_NAME}", - ], - }, - # has to be kept in sync with the version in src/hugo/cli.py - version=HUGO_VERSION, -) diff --git a/src/hugo/_version.py.in b/src/hugo/_version.py.in new file mode 100644 index 0000000..a0f37ea --- /dev/null +++ b/src/hugo/_version.py.in @@ -0,0 +1 @@ +HUGO_VERSION = "@HUGO_VERSION@" diff --git a/src/hugo/cli.py b/src/hugo/cli.py index 093cade..ad1a086 100644 --- a/src/hugo/cli.py +++ b/src/hugo/cli.py @@ -6,79 +6,78 @@ from __future__ import annotations +import json import os +import sys +import sysconfig +from contextlib import nullcontext +from pathlib import Path, PurePosixPath from sys import platform as sysplatform -HUGO_VERSION = "0.160.1" -FILE_EXT = ".exe" if sysplatform == "win32" else "" -if sysplatform == "win32": - HUGO_PLATFORM = "windows" -elif sysplatform == "linux": - HUGO_PLATFORM = "linux" -else: - HUGO_PLATFORM = "darwin" +from hugo._version import HUGO_VERSION +HUGO_EXECUTABLE = "hugo.exe" if sysplatform == "win32" else "hugo" +HUGO_BINARY_PATH = Path("hugo", "binaries", HUGO_EXECUTABLE) -def get_hugo_arch(): - from sys import maxsize as sysmaxsize - if sysplatform == "win32": - from platform import machine +def _editable_hugo_executable() -> Path | None: + """Resolve the bundled binary from meson-python's editable install tree.""" + for finder in sys.meta_path: + if type(finder).__name__ != "MesonpyMetaFinder": + continue + if getattr(finder, "_name", None) != "hugo": + continue - m = machine() - else: - m = os.uname().machine + finder._rebuild() - HUGO_ARCH = { - "x86_64": "amd64", - "arm64": "arm64", - "AMD64": "amd64", - "aarch64": "arm64", - "x86": "386", - "i686": "386", - "i386": "386", - "s390x": "s390x", - "ppc64le": "ppc64le", - "armv7l": "arm", - "armv6l": "arm", - "riscv64": "riscv64", - }[m] + install_plan = Path(finder._build_path, "meson-info", "intro-install_plan.json") + if not install_plan.is_file(): + continue - # platform.machine returns AMD64 on Windows because the architecture is - # 64-bit (even if one is running a 32-bit Python interpreter). Therefore - # we use sys.maxsize to handle this special case on Windows + with install_plan.open(encoding="utf-8") as file: + plan = json.load(file) - if not (sysmaxsize > 2**32) and sysplatform == "win32": - HUGO_ARCH = "386" + for targets in plan.values(): + for source, target in targets.items(): + destination = PurePosixPath(target["destination"].replace("\\", "/")) + if destination.parts[-3:] == ("hugo", "binaries", HUGO_EXECUTABLE): + return Path(source) - return HUGO_ARCH + return None -HUGO_ARCH = get_hugo_arch() +def _hugo_executable(): + data_path = Path(sysconfig.get_path("data")) + binary = data_path / HUGO_BINARY_PATH + if binary.is_file(): + return nullcontext(binary) -DIR = os.path.dirname(os.path.abspath(__file__)) # noqa: PTH100, PTH120 + editable_binary = _editable_hugo_executable() + if editable_binary is not None: + return nullcontext(editable_binary) -HUGO_EXECUTABLE = os.path.join( # noqa: PTH118 - DIR, "binaries", f"hugo-{HUGO_VERSION}-{HUGO_PLATFORM}-{HUGO_ARCH}{FILE_EXT}" -) + raise FileNotFoundError(binary) def __call(): """ Hugo binary entry point. Passes all command-line arguments to Hugo. """ - print( - f"\033[95mRunning Hugo {HUGO_VERSION} via hugo-python-distributions at {HUGO_EXECUTABLE}\033[0m" - ) + with _hugo_executable() as hugo_executable: + hugo_executable_str = os.fspath(hugo_executable) - from sys import argv as sysargv + print( + f"\033[95mRunning Hugo {HUGO_VERSION} via hugo-python-distributions at {hugo_executable_str}\033[0m" + ) - if sysplatform == "win32": - from subprocess import check_call + from sys import argv as sysargv - check_call([HUGO_EXECUTABLE, *sysargv[1:]]) - else: - os.execv(HUGO_EXECUTABLE, ["hugo", *sysargv[1:]]) + if sysplatform == "win32": + from subprocess import check_call + + check_call([hugo_executable_str, *sysargv[1:]]) + else: + os.execv(hugo_executable_str, ["hugo", *sysargv[1:]]) if __name__ == "__main__":