diff --git a/.github/actions/boost/action.yaml b/.github/actions/boost/action.yaml new file mode 100644 index 000000000..6cd078be0 --- /dev/null +++ b/.github/actions/boost/action.yaml @@ -0,0 +1,173 @@ +--- +name: Build Boost +description: Download and build Boost C++ libraries (Windows/MSVC). +inputs: + version: + description: Boost version to build (e.g. 1.86.0) + required: true + architecture: + description: Target architecture (x86, x64 or arm64) + required: true + toolset: + description: | + MSVC toolset version passed to b2 (e.g. 14.1 for v141, 14.3 for v143). + required: true + libraries: + description: Space-separated list of Boost libraries (without the `--with-` prefix). + required: true + link: + description: shared or static + required: false + default: shared + runtime-link: + description: shared or static + required: false + default: shared + target_folder: + description: | + Directory (relative to workspace) used as both the unpacked Boost source + tree and the cache key. Defaults to `tmp/boost`. Use a unique value for + each independent build (e.g. `tmp/static_boost`). + required: false + default: tmp/boost + python-version: + description: | + Python major.minor (e.g. `3.11`). Required if `python` is in `libraries` + *and* you want to override the auto-detected interpreter (e.g. when + cross-compiling). Combined with `python-include` / `python-libs` it + writes a `user-config.jam` so b2 picks up the right headers/libs. + required: false + default: '' + python-include: + description: Python include directory for boost.python (cross-compile). + required: false + default: '' + python-libs: + description: Python import library directory for boost.python (cross-compile). + required: false + default: '' + +outputs: + root: + description: Root Boost directory (Windows path). + value: ${{ steps.path.outputs.root }} + root_unix: + description: Root Boost directory (forward-slash path). + value: ${{ steps.path.outputs.root_unix }} + librarydir: + description: Directory containing built Boost libraries (Windows path). + value: ${{ steps.path.outputs.librarydir }} + librarydir_unix: + description: Directory containing built Boost libraries (forward-slash path). + value: ${{ steps.path.outputs.librarydir_unix }} + +runs: + using: composite + steps: + + - id: cache + uses: actions/cache@v3 + with: + key: ${{ runner.os }}-boost-${{ inputs.version }}-${{ inputs.architecture }}-${{ inputs.toolset }}-${{ inputs.link }}-${{ inputs.runtime-link }}-py${{ inputs.python-version }}-${{ hashFiles('.github/actions/boost/action.yaml') }}-${{ inputs.libraries }} + path: | + ${{ inputs.target_folder }} + + - id: setup + shell: pwsh + run: | + $arch = '${{ inputs.architecture }}' + switch ($arch) { + 'x86' { $b2_arch = @('address-model=32'); $stage = 'x86' } + 'x64' { $b2_arch = @('address-model=64'); $stage = 'x64' } + 'arm64' { $b2_arch = @('architecture=arm', 'address-model=64'); $stage = 'arm64' } + default { Write-Error "Unsupported architecture '$arch'"; exit 1 } + } + + $version = '${{ inputs.version }}' + $underscore = $version.Replace('.', '_') + $stagedir = "stage/$stage/Release" + + echo "b2_arch=$($b2_arch -join ' ')" >> $env:GITHUB_OUTPUT + echo "stage=$stage" >> $env:GITHUB_OUTPUT + echo "stagedir=$stagedir" >> $env:GITHUB_OUTPUT + echo "version_underscore=$underscore" >> $env:GITHUB_OUTPUT + + - name: Download Boost + if: steps.cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $version = '${{ inputs.version }}' + $underscore = '${{ steps.setup.outputs.version_underscore }}' + $target = '${{ inputs.target_folder }}' + $parent = Split-Path $target + if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } + if (Test-Path $target) { Remove-Item -Recurse -Force $target } + + $archive = Join-Path $env:RUNNER_TEMP "boost_$underscore.tar.gz" + if (-not (Test-Path $archive)) { + $url = "https://archives.boost.io/release/$version/source/boost_$underscore.tar.gz" + Write-Host "Downloading $url" + Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $archive + } + + Write-Host "Extracting $archive to $parent" + tar -xzf $archive -C $parent + Move-Item (Join-Path $parent "boost_$underscore") $target + + - name: Bootstrap b2 + if: steps.cache.outputs.cache-hit != 'true' + shell: cmd + working-directory: ${{ inputs.target_folder }} + run: | + if not exist b2.exe call bootstrap.bat + + - name: Build Boost + if: steps.cache.outputs.cache-hit != 'true' + shell: pwsh + working-directory: ${{ inputs.target_folder }} + run: | + $ErrorActionPreference = 'Stop' + $libs = '${{ inputs.libraries }}'.Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) | + ForEach-Object { "--with-$_" } + $arch = '${{ steps.setup.outputs.b2_arch }}'.Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) + $b2args = @("toolset=msvc-${{ inputs.toolset }}") + $arch + @( + 'variant=release', + "link=${{ inputs.link }}", + "runtime-link=${{ inputs.runtime-link }}", + 'threading=multi', + '--layout=system', + "--stagedir=${{ steps.setup.outputs.stagedir }}" + ) + + # Optional: user-config.jam for cross-compiled boost.python. + $py_version = '${{ inputs.python-version }}' + $py_include = '${{ inputs.python-include }}' + $py_libs = '${{ inputs.python-libs }}' + if ($py_version -and $py_include -and $py_libs) { + $inc_fwd = $py_include.Replace('\','/') + $lib_fwd = $py_libs.Replace('\','/') + $jam = "using python : $py_version : python : `"$inc_fwd`" : `"$lib_fwd`" ;`n" + $jam_path = Join-Path (Resolve-Path '.').Path 'user-config.jam' + Set-Content -Path $jam_path -Value $jam -Encoding ASCII + Write-Host "Wrote $jam_path :" + Get-Content $jam_path + $b2args += "--user-config=$jam_path" + } + + $b2args += $libs + @('-d0', 'warnings=off', 'stage') + Write-Host "Running: b2 $($b2args -join ' ')" + & .\b2.exe @b2args + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - id: path + shell: pwsh + run: | + $root = (Resolve-Path '${{ inputs.target_folder }}').Path + $lib = Join-Path $root '${{ steps.setup.outputs.stagedir }}/lib' + echo "root=$root" >> $env:GITHUB_OUTPUT + echo "root_unix=$($root.Replace('\','/'))" >> $env:GITHUB_OUTPUT + echo "librarydir=$lib" >> $env:GITHUB_OUTPUT + echo "librarydir_unix=$($lib.Replace('\','/'))" >> $env:GITHUB_OUTPUT + + diff --git a/.github/workflows/build-feature.yml b/.github/workflows/build-feature.yml index 6323a6227..8792fc077 100644 --- a/.github/workflows/build-feature.yml +++ b/.github/workflows/build-feature.yml @@ -65,6 +65,27 @@ jobs: AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + build-client-arm64: + needs: get-version + uses: ./.github/workflows/build-rust.yml + with: + os: windows-latest + arch: arm64 + version: ${{ needs.get-version.outputs.version }} + secrets: + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + build-server-arm64: + needs: [get-version, build-client-arm64, create-test-bundle] + uses: ./.github/workflows/build-windows.yml + with: + architecture: arm64 + version: ${{ needs.get-version.outputs.version }} + secrets: + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} build-client-ubuntu-x64: needs: get-version uses: ./.github/workflows/build-rust.yml diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 2c62e4f3b..95514ac4e 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -28,6 +28,9 @@ on: env: PYTHON_VERSION: 3.11 + # Exact ARM64 Python version pulled from the python.org NuGet feed when + # cross-compiling for arm64 (we need the matching python311.lib + headers). + PYTHON_ARM64_VERSION: 3.11.9 OPENSSL_VERSION: 3.5.4 PROTOBUF_VERSION: 21.12 BOOST_VERSION: 1.86.0 @@ -178,40 +181,96 @@ jobs: with: version: ${{ ENV.MINIZ_VERSION }} + # ----- Boost build ----- + # Built locally via .github/actions/boost (no external action) so that + # ARM64 cross-compilation, toolset selection and caching are all under + # our control. + # + # For arm64 we install the official `pythonarm64` NuGet package and + # point boost.python at its headers and import library so the resulting + # boost_python311.dll links against ARM64 python311.lib. The interpreter + # itself is never executed during the build - host x64 Python is still + # used for everything that runs locally (mk_pyzip.py, etc.). + - name: Install ARM64 Python (cross-compile target) + id: arm64_python + if: inputs.architecture == 'arm64' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $version = '${{ env.PYTHON_ARM64_VERSION }}' + $dest = Join-Path $env:GITHUB_WORKSPACE 'tmp/python-arm64' + New-Item -ItemType Directory -Force -Path $dest | Out-Null + nuget install pythonarm64 -Version $version -OutputDirectory $dest -ExcludeVersion -DependencyVersion Ignore + $root = Join-Path $dest 'pythonarm64\tools' + $include = Join-Path $root 'include' + $libs = Join-Path $root 'libs' + if (-not (Test-Path $include)) { Write-Error "ARM64 Python include not found at $include"; exit 1 } + if (-not (Test-Path $libs)) { Write-Error "ARM64 Python libs not found at $libs"; exit 1 } + $major_minor = ($version -split '\.')[0..1] -join '.' + echo "root=$root" >> $env:GITHUB_OUTPUT + echo "include=$include" >> $env:GITHUB_OUTPUT + echo "libs=$libs" >> $env:GITHUB_OUTPUT + echo "root_unix=$($root.Replace('\','/'))" >> $env:GITHUB_OUTPUT + echo "major_minor=$major_minor" >> $env:GITHUB_OUTPUT + - name: Build Boost (static) id: static_boost - uses: mickem/build-boost@v1 + uses: ./.github/actions/boost with: - version: ${{ ENV.BOOST_VERSION }} + version: ${{ env.BOOST_VERSION }} + architecture: ${{ inputs.architecture }} + toolset: ${{ steps.setup.outputs.vs_toolset }} libraries: system filesystem - platform: ${{ inputs.architecture }} - configuration: Release - static: 1 - static-runtime: 1 - directory: ${{ runner.workspace }}/static_boost - - - name: Build Boost (regular) - id: boost - uses: mickem/build-boost@v1 + link: static + runtime-link: static + target_folder: tmp/static_boost + + - name: Build Boost (regular, x86/x64) + id: boost_xx + if: inputs.architecture != 'arm64' + uses: ./.github/actions/boost + with: + version: ${{ env.BOOST_VERSION }} + architecture: ${{ inputs.architecture }} + toolset: ${{ steps.setup.outputs.vs_toolset }} + libraries: system filesystem thread regex date_time program_options python chrono json + link: shared + runtime-link: shared + target_folder: tmp/boost + + - name: Build Boost (regular, arm64) + id: boost_arm64 + if: inputs.architecture == 'arm64' + uses: ./.github/actions/boost with: - version: ${{ ENV.BOOST_VERSION }} + version: ${{ env.BOOST_VERSION }} + architecture: ${{ inputs.architecture }} + toolset: ${{ steps.setup.outputs.vs_toolset }} libraries: system filesystem thread regex date_time program_options python chrono json - platform: ${{ inputs.architecture }} - configuration: Release - + link: shared + runtime-link: shared + target_folder: tmp/boost + python-version: ${{ steps.arm64_python.outputs.major_minor }} + python-include: ${{ steps.arm64_python.outputs.include }} + python-libs: ${{ steps.arm64_python.outputs.libs }} + - id: paths - run: | - $path_unix="${{ steps.boost.outputs.root }}".replace('\','/') - echo "boost_root=$path_unix" >> $env:GITHUB_OUTPUT - $path_unix="${{ steps.boost.outputs.librarydir }}".replace('\','/') - echo "boost_librarydir=$path_unix" >> $env:GITHUB_OUTPUT - $path_unix="${{ steps.static_boost.outputs.root }}".replace('\','/') - echo "static_boost_root=$path_unix" >> $env:GITHUB_OUTPUT - $path_unix="${{ steps.static_boost.outputs.librarydir }}".replace('\','/') - echo "static_boost_librarydir=$path_unix" >> $env:GITHUB_OUTPUT - $path_unix="${{ env.Python_ROOT_DIR }}".replace('\','/') - echo "python_path=$path_unix" >> $env:GITHUB_OUTPUT shell: pwsh + run: | + if ('${{ inputs.architecture }}' -eq 'arm64') { + $boost_root_unix = '${{ steps.boost_arm64.outputs.root_unix }}' + $boost_librarydir_unix = '${{ steps.boost_arm64.outputs.librarydir_unix }}' + $python_path = '${{ steps.arm64_python.outputs.root_unix }}' + } else { + $boost_root_unix = '${{ steps.boost_xx.outputs.root_unix }}' + $boost_librarydir_unix = '${{ steps.boost_xx.outputs.librarydir_unix }}' + $python_path = '${{ env.Python_ROOT_DIR }}'.Replace('\','/') + } + echo "boost_root=$boost_root_unix" >> $env:GITHUB_OUTPUT + echo "boost_librarydir=$boost_librarydir_unix" >> $env:GITHUB_OUTPUT + echo "static_boost_root=${{ steps.static_boost.outputs.root_unix }}" >> $env:GITHUB_OUTPUT + echo "static_boost_librarydir=${{ steps.static_boost.outputs.librarydir_unix }}" >> $env:GITHUB_OUTPUT + echo "python_path=$python_path" >> $env:GITHUB_OUTPUT - uses: DamianReeves/write-file-action@master name: Write NSClient++ cmake config