diff --git a/.github/workflows/build-python-packages.yml b/.github/workflows/build-python-packages.yml index 3f24c128..02360c00 100644 --- a/.github/workflows/build-python-packages.yml +++ b/.github/workflows/build-python-packages.yml @@ -12,6 +12,11 @@ on: required: true type: boolean default: false + THREADING_BUILD_MODES: + description: 'CPython threading build modes' + required: true + type: str + default: 'default,freethreaded' PLATFORMS: description: 'Platforms for execution in "os" or "os_arch" format (arch is "x64" by default)' required: true @@ -40,32 +45,42 @@ jobs: id: generate-matrix run: | [String[]]$configurations = "${{ inputs.platforms || 'ubuntu-20.04,ubuntu-22.04,ubuntu-22.04_arm64,ubuntu-24.04,ubuntu-24.04_arm64,macos-13,macos-14_arm64,windows-2019_x64,windows-2019_x86,windows-2019_arm64' }}".Split(",").Trim() + [String[]]$buildModes = "${{ inputs.threading_build_modes || 'default' }}".Split(",").Trim() $matrix = @() foreach ($configuration in $configurations) { - $parts = $configuration.Split("_") - $os = $parts[0] - $arch = if ($parts[1]) {$parts[1]} else {"x64"} - switch -wildcard ($os) { - "*ubuntu*" { $platform = $os.Replace("ubuntu","linux")} - "*macos*" { $platform = 'darwin' } - "*windows*" { $platform = 'win32' } - } - - if ($configuration -eq "ubuntu-22.04_arm64") { - $os = "setup-actions-ubuntu-arm64-2-core" - } - elseif ($configuration -eq "ubuntu-24.04_arm64") { - $os = "setup-actions-ubuntu24-arm64-2-core" - } - elseif ($configuration -eq "windows-2019_arm64") { - $os = "setup-actions-windows-arm64-4-core" - } - - $matrix += @{ - 'platform' = $platform - 'os' = $os - 'arch' = $arch + foreach ($buildMode in $buildModes) { + $parts = $configuration.Split("_") + $os = $parts[0] + $arch = if ($parts[1]) {$parts[1]} else {"x64"} + switch -wildcard ($os) { + "*ubuntu*" { $platform = $os.Replace("ubuntu","linux")} + "*macos*" { $platform = 'darwin' } + "*windows*" { $platform = 'win32' } + } + + if ($configuration -eq "ubuntu-22.04_arm64") { + $os = "setup-actions-ubuntu-arm64-2-core" + } + elseif ($configuration -eq "ubuntu-24.04_arm64") { + $os = "setup-actions-ubuntu24-arm64-2-core" + } + elseif ($configuration -eq "windows-2019_arm64") { + $os = "setup-actions-windows-arm64-4-core" + } + + if ($buildMode -eq "freethreaded") { + if ([semver]"${{ inputs.VERSION }}" -lt [semver]"3.13.0") { + continue; + } + $arch += "-freethreaded" + } + + $matrix += @{ + 'platform' = $platform + 'os' = $os + 'arch' = $arch + } } } echo "matrix=$($matrix | ConvertTo-Json -Compress -AsArray)" >> $env:GITHUB_OUTPUT @@ -201,6 +216,9 @@ jobs: python-version: ${{ env.VERSION }} architecture: ${{ matrix.arch }} + - name: Python version + run: python -VVV + - name: Verbose sysconfig dump if: runner.os == 'Linux' || runner.os == 'macOS' run: python ./sources/python-config-output.py diff --git a/builders/macos-python-builder.psm1 b/builders/macos-python-builder.psm1 index 6b36fddc..fe442c48 100644 --- a/builders/macos-python-builder.psm1 +++ b/builders/macos-python-builder.psm1 @@ -151,6 +151,37 @@ class macOSPythonBuilder : NixPythonBuilder { return $pkgLocation } + [string] GetFrameworkName() { + <# + .SYNOPSIS + Get the Python installation Package name. + #> + + if ($this.IsFreeThreaded()) { + return "PythonT.framework" + } else { + return "Python.framework" + } + } + + [string] GetPkgChoices() { + <# + .SYNOPSIS + Reads the configuration XML file for the Python installer + #> + + $config = if ($this.IsFreeThreaded()) { "freethreaded" } else { "default" } + $choicesFile = Join-Path $PSScriptRoot "../config/macos-pkg-choices-$($config).xml" + $choicesTemplate = Get-Content -Path $choicesFile -Raw + + $variablesToReplace = @{ + "{{__VERSION_MAJOR_MINOR__}}" = "$($this.Version.Major).$($this.Version.Minor)"; + } + + $variablesToReplace.keys | ForEach-Object { $choicesTemplate = $choicesTemplate.Replace($_, $variablesToReplace[$_]) } + return $choicesTemplate + } + [void] CreateInstallationScriptPkg() { <# .SYNOPSIS @@ -165,6 +196,8 @@ class macOSPythonBuilder : NixPythonBuilder { "{{__VERSION_FULL__}}" = $this.Version; "{{__PKG_NAME__}}" = $this.GetPkgName(); "{{__ARCH__}}" = $this.Architecture; + "{{__FRAMEWORK_NAME__}}" = $this.GetFrameworkName(); + "{{__PKG_CHOICES__}}" = $this.GetPkgChoices(); } $variablesToReplace.keys | ForEach-Object { $installationTemplateContent = $installationTemplateContent.Replace($_, $variablesToReplace[$_]) } diff --git a/builders/nix-python-builder.psm1 b/builders/nix-python-builder.psm1 index b15878e0..ea73539d 100644 --- a/builders/nix-python-builder.psm1 +++ b/builders/nix-python-builder.psm1 @@ -115,7 +115,7 @@ class NixPythonBuilder : PythonBuilder { Write-Debug "make Python $($this.Version)-$($this.Architecture) $($this.Platform)" $buildOutputLocation = New-Item -Path $this.WorkFolderLocation -Name "build_output.txt" -ItemType File - Execute-Command -Command "make 2>&1 | tee $buildOutputLocation" -ErrorAction Continue + Execute-Command -Command "make 2>&1 | tee $buildOutputLocation" -ErrorAction Continue Execute-Command -Command "make install" -ErrorAction Continue Write-Debug "Done; Make log location: $buildOutputLocation" diff --git a/builders/python-builder.psm1 b/builders/python-builder.psm1 index c2541d37..fb8fe7b4 100644 --- a/builders/python-builder.psm1 +++ b/builders/python-builder.psm1 @@ -94,6 +94,24 @@ class PythonBuilder { return "$($this.Version.Major).$($this.Version.Minor).$($this.Version.Patch)" } + [string] GetHardwareArchitecture() { + <# + .SYNOPSIS + The hardware architecture (x64, arm64) without any Python free threading suffix. + #> + + return $this.Architecture.Replace("-freethreaded", "") + } + + [bool] IsFreeThreaded() { + <# + .SYNOPSIS + Check if Python version is free threaded. + #> + + return $this.Architecture.EndsWith("-freethreaded") + } + [void] PreparePythonToolcacheLocation() { <# .SYNOPSIS diff --git a/builders/ubuntu-python-builder.psm1 b/builders/ubuntu-python-builder.psm1 index 35b159c5..d7b13eb0 100644 --- a/builders/ubuntu-python-builder.psm1 +++ b/builders/ubuntu-python-builder.psm1 @@ -37,6 +37,14 @@ class UbuntuPythonBuilder : NixPythonBuilder { $configureString += " --enable-shared" $configureString += " --enable-optimizations" + if ($this.IsFreeThreaded()) { + if ($this.Version -lt "3.13.0") { + Write-Host "Python versions lower than 3.13.0 do not support free threading" + exit 1 + } + $configureString += " --disable-gil" + } + ### Compile with support of loadable sqlite extensions. ### Link to documentation (https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.enable_load_extension) $configureString += " --enable-loadable-sqlite-extensions" diff --git a/builders/win-python-builder.psm1 b/builders/win-python-builder.psm1 index 95d11bc3..9614764a 100644 --- a/builders/win-python-builder.psm1 +++ b/builders/win-python-builder.psm1 @@ -54,13 +54,13 @@ class WinPythonBuilder : PythonBuilder { #> $ArchitectureExtension = "" - if ($this.Architecture -eq "x64") { + if ($this.GetHardwareArchitecture() -eq "x64") { if ($this.Version -ge "3.5") { $ArchitectureExtension = "-amd64" } else { $ArchitectureExtension = ".amd64" } - }elseif ($this.Architecture -eq "arm64") { + } elseif ($this.GetHardwareArchitecture() -eq "arm64") { $ArchitectureExtension = "-arm64" } @@ -113,6 +113,7 @@ class WinPythonBuilder : PythonBuilder { $variablesToReplace = @{ "{{__ARCHITECTURE__}}" = $this.Architecture; + "{{__HARDWARE_ARCHITECTURE__}}" = $this.GetHardwareArchitecture(); "{{__VERSION__}}" = $this.Version; "{{__PYTHON_EXEC_NAME__}}" = $pythonExecName } diff --git a/config/macos-pkg-choices-default.xml b/config/macos-pkg-choices-default.xml new file mode 100644 index 00000000..4cfb0166 --- /dev/null +++ b/config/macos-pkg-choices-default.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/config/macos-pkg-choices-freethreaded.xml b/config/macos-pkg-choices-freethreaded.xml new file mode 100644 index 00000000..1a6f403c --- /dev/null +++ b/config/macos-pkg-choices-freethreaded.xml @@ -0,0 +1,14 @@ + + + + + + attributeSetting + 1 + choiceAttribute + selected + choiceIdentifier + org.python.Python.PythonTFramework-{{__VERSION_MAJOR_MINOR__}} + + + diff --git a/config/python-manifest-config.json b/config/python-manifest-config.json index d2529b00..7390b920 100644 --- a/config/python-manifest-config.json +++ b/config/python-manifest-config.json @@ -1,5 +1,5 @@ { - "regex": "python-\\d+\\.\\d+\\.\\d+-(\\w+\\.\\d+)?-?(\\w+)-(\\d+\\.\\d+)?-?((x|arm)\\d+)", + "regex": "python-\\d+\\.\\d+\\.\\d+-(\\w+\\.\\d+)?-?(\\w+)-(\\d+\\.\\d+)?-?((x|arm)\\d+(-freethreaded)?)", "groups": { "arch": 4, "platform": 2, diff --git a/installers/macos-pkg-setup-template.sh b/installers/macos-pkg-setup-template.sh index 5e1aa6ef..a6c909ee 100644 --- a/installers/macos-pkg-setup-template.sh +++ b/installers/macos-pkg-setup-template.sh @@ -2,6 +2,11 @@ set -e PYTHON_FULL_VERSION="{{__VERSION_FULL__}}" PYTHON_PKG_NAME="{{__PKG_NAME__}}" +PYTHON_FRAMEWORK_NAME="{{__FRAMEWORK_NAME__}}" +PYTHON_PKG_CHOICES=$(cat << 'EOF' +{{__PKG_CHOICES__}} +EOF +) ARCH="{{__ARCH__}}" MAJOR_VERSION=$(echo $PYTHON_FULL_VERSION | cut -d '.' -f 1) MINOR_VERSION=$(echo $PYTHON_FULL_VERSION | cut -d '.' -f 2) @@ -20,7 +25,7 @@ fi PYTHON_TOOLCACHE_PATH=$TOOLCACHE_ROOT/Python PYTHON_TOOLCACHE_VERSION_PATH=$PYTHON_TOOLCACHE_PATH/$PYTHON_FULL_VERSION PYTHON_TOOLCACHE_VERSION_ARCH_PATH=$PYTHON_TOOLCACHE_VERSION_PATH/$ARCH -PYTHON_FRAMEWORK_PATH="/Library/Frameworks/Python.framework/Versions/${MAJOR_VERSION}.${MINOR_VERSION}" +PYTHON_FRAMEWORK_PATH="/Library/Frameworks/${PYTHON_FRAMEWORK_NAME}/Versions/${MAJOR_VERSION}.${MINOR_VERSION}" PYTHON_APPLICATION_PATH="/Applications/Python ${MAJOR_VERSION}.${MINOR_VERSION}" echo "Check if Python hostedtoolcache folder exist..." @@ -38,8 +43,11 @@ else done fi +PYTHON_PKG_CHOICES_FILES=$(mktemp) +echo "$PYTHON_PKG_CHOICES" > $PYTHON_PKG_CHOICES_FILES + echo "Install Python binaries from prebuilt package" -sudo installer -pkg $PYTHON_PKG_NAME -target / +sudo installer -pkg $PYTHON_PKG_NAME -applyChoiceChangesXML $PYTHON_PKG_CHOICES_FILES -target / echo "Create hostedtoolcach symlinks (Required for the backward compatibility)" echo "Create Python $PYTHON_FULL_VERSION folder" @@ -53,7 +61,9 @@ ln -s "${PYTHON_FRAMEWORK_PATH}/lib" lib echo "Create additional symlinks (Required for the UsePythonVersion Azure Pipelines task and the setup-python GitHub Action)" ln -s ./bin/$PYTHON_MAJOR_DOT_MINOR python +chmod +x python +# Note that bin is a symlink so referencing .. from bin will not work as expected cd bin/ # This symlink already exists if Python version with the same major.minor version is installed, @@ -62,11 +72,15 @@ if [ ! -f $PYTHON_MAJOR_MINOR ]; then ln -s $PYTHON_MAJOR_DOT_MINOR $PYTHON_MAJOR_MINOR fi +if [ ! -f $PYTHON_MAJOR ]; then + ln -s $PYTHON_MAJOR_DOT_MINOR $PYTHON_MAJOR +fi + if [ ! -f python ]; then ln -s $PYTHON_MAJOR_DOT_MINOR python fi -chmod +x ../python $PYTHON_MAJOR $PYTHON_MAJOR_DOT_MINOR $PYTHON_MAJOR_MINOR python +chmod +x $PYTHON_MAJOR $PYTHON_MAJOR_DOT_MINOR $PYTHON_MAJOR_MINOR python echo "Upgrading pip..." export PIP_ROOT_USER_ACTION=ignore diff --git a/installers/win-setup-template.ps1 b/installers/win-setup-template.ps1 index e2a33b8b..5aaad80e 100644 --- a/installers/win-setup-template.ps1 +++ b/installers/win-setup-template.ps1 @@ -1,4 +1,5 @@ [String] $Architecture = "{{__ARCHITECTURE__}}" +[String] $HardwareArchitecture = "{{__HARDWARE_ARCHITECTURE__}}" [String] $Version = "{{__VERSION__}}" [String] $PythonExecName = "{{__PYTHON_EXEC_NAME__}}" @@ -25,7 +26,7 @@ function Remove-RegistryEntries { [Parameter(Mandatory)][Int32] $MinorVersion ) - $versionFilter = Get-RegistryVersionFilter -Architecture $Architecture -MajorVersion $MajorVersion -MinorVersion $MinorVersion + $versionFilter = Get-RegistryVersionFilter -Architecture $HardwareArchitecture -MajorVersion $MajorVersion -MinorVersion $MinorVersion $regPath = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products" if (Test-Path -Path Registry::$regPath) { @@ -61,13 +62,15 @@ function Remove-RegistryEntries { function Get-ExecParams { param( [Parameter(Mandatory)][Boolean] $IsMSI, + [Parameter(Mandatory)][Boolean] $IsFreeThreaded, [Parameter(Mandatory)][String] $PythonArchPath ) if ($IsMSI) { "TARGETDIR=$PythonArchPath ALLUSERS=1" } else { - "DefaultAllUsersTargetDir=$PythonArchPath InstallAllUsers=1" + $Include_freethreaded = if ($IsFreeThreaded) { "Include_freethreaded=1" } else { "" } + "DefaultAllUsersTargetDir=$PythonArchPath InstallAllUsers=1 $Include_freethreaded" } } @@ -81,6 +84,7 @@ $PythonVersionPath = Join-Path -Path $PythonToolcachePath -ChildPath $Version $PythonArchPath = Join-Path -Path $PythonVersionPath -ChildPath $Architecture $IsMSI = $PythonExecName -match "msi" +$IsFreeThreaded = $Architecture -match "-freethreaded" $MajorVersion = $Version.Split('.')[0] $MinorVersion = $Version.Split('.')[1] @@ -120,13 +124,24 @@ Write-Host "Copy Python binaries to $PythonArchPath" Copy-Item -Path ./$PythonExecName -Destination $PythonArchPath | Out-Null Write-Host "Install Python $Version in $PythonToolcachePath..." -$ExecParams = Get-ExecParams -IsMSI $IsMSI -PythonArchPath $PythonArchPath +$ExecParams = Get-ExecParams -IsMSI $IsMSI -IsFreeThreaded $IsFreeThreaded -PythonArchPath $PythonArchPath cmd.exe /c "cd $PythonArchPath && call $PythonExecName $ExecParams /quiet" if ($LASTEXITCODE -ne 0) { Throw "Error happened during Python installation" } +# print out all files in $PythonArchPath +Write-Host "Files in $PythonArchPath" +$files = Get-ChildItem -Path $PythonArchPath -File -Recurse +Write-Output $files + +if ($IsFreeThreaded) { + # Delete python.exe and create a symlink to free-threaded exe + Remove-Item -Path "$PythonArchPath\python.exe" -Force + New-Item -Path "$PythonArchPath\python.exe" -ItemType SymbolicLink -Value "$PythonArchPath\python${MajorVersion}.${MinorVersion}t.exe" +} + Write-Host "Create `python3` symlink" if ($MajorVersion -ne "2") { New-Item -Path "$PythonArchPath\python3.exe" -ItemType SymbolicLink -Value "$PythonArchPath\python.exe" diff --git a/tests/python-tests.ps1 b/tests/python-tests.ps1 index 706a3b42..0f9d2abe 100644 --- a/tests/python-tests.ps1 +++ b/tests/python-tests.ps1 @@ -7,6 +7,8 @@ param ( $Architecture ) +$HardwareArchitecture = $Architecture -replace "-freethreaded", "" + Import-Module (Join-Path $PSScriptRoot "../helpers/pester-extensions.psm1") Import-Module (Join-Path $PSScriptRoot "../helpers/common-helpers.psm1") Import-Module (Join-Path $PSScriptRoot "../builders/python-version.psm1") @@ -58,7 +60,7 @@ Describe "Tests" { # } # } - if (($Version -ge "3.2.0") -and ($Version -lt "3.11.0") -and (($Platform -ne "darwin") -or ($Architecture -ne "arm64"))) { + if (($Version -ge "3.2.0") -and ($Version -lt "3.11.0") -and (($Platform -ne "darwin") -or ($HardwareArchitecture -ne "arm64"))) { It "Check if sqlite3 module is installed" { "python ./sources/python-sqlite3.py" | Should -ReturnZeroExitCode } diff --git a/tests/sources/python-config-test.py b/tests/sources/python-config-test.py index ea40a8ae..c1030098 100644 --- a/tests/sources/python-config-test.py +++ b/tests/sources/python-config-test.py @@ -8,25 +8,31 @@ version = sys.argv[1] nativeVersion = sys.argv[2] architecture = sys.argv[3] +hw_architecture = architecture.replace('-freethreaded', '') versions=version.split(".") version_major=int(versions[0]) version_minor=int(versions[1]) -pkg_installer = os_type == 'Darwin' and ((version_major == 3 and version_minor >= 11) or (architecture == "arm64")) +pkg_installer = os_type == 'Darwin' and ((version_major == 3 and version_minor >= 11) or (hw_architecture == "arm64")) lib_dir_path = sysconfig.get_config_var('LIBDIR') ld_library_name = sysconfig.get_config_var('LDLIBRARY') is_shared = sysconfig.get_config_var('Py_ENABLE_SHARED') have_libreadline = sysconfig.get_config_var("HAVE_LIBREADLINE") +is_free_threaded = sysconfig.get_config_var('Py_GIL_DISABLED') ### Define expected variables if os_type == 'Linux': expected_ld_library_extension = 'so' if os_type == 'Darwin': expected_ld_library_extension = 'dylib' +if is_free_threaded: + framework_name = 'PythonT.framework' +else: + framework_name = 'Python.framework' if pkg_installer: - expected_lib_dir_path = f'/Library/Frameworks/Python.framework/Versions/{version_major}.{version_minor}/lib' + expected_lib_dir_path = f'/Library/Frameworks/{framework_name}/Versions/{version_major}.{version_minor}/lib' else: expected_lib_dir_path = f'{os.getenv("AGENT_TOOLSDIRECTORY")}/Python/{version}/{architecture}/lib'