diff options
author | Egor Tensin <Egor.Tensin@gmail.com> | 2021-01-17 13:54:57 +0300 |
---|---|---|
committer | Egor Tensin <Egor.Tensin@gmail.com> | 2021-01-17 13:54:57 +0300 |
commit | dd2c5b58c4fe77d7ce35f3abb6e1bb399560a2db (patch) | |
tree | 813808b873a5895d7f212f890c68ff14820dc591 | |
parent | Travis/AppVeyor: pause (diff) | |
download | cmake-common-dd2c5b58c4fe77d7ce35f3abb6e1bb399560a2db.tar.gz cmake-common-dd2c5b58c4fe77d7ce35f3abb6e1bb399560a2db.zip |
GIANT CLUSTERFUCK OF A COMMIT
OK, this is epic. I was basically just trying to a) support Clang and
b) add more test coverage. _THREE MONTHS_ and a few hundred CI runs
later, this is what I came up with. I don't know how it ended up being
what it is, but here we go.
Some highlights of the changes:
1) CI builds has been moved to GitHub Actions,
2) the entire notion of a toolchain has been reworked; it now supports
Clang on all platforms.
* .github: this directory contains the GitHub Actions workflow
scripts/actions. In the process, I created like 6 external GitHub
actions, but it's still pretty massive. An upside is that it covers
much more platform/toolchain combinations _and_ check a lot of the
expected post-conditions. TODO: .ci/Makefile is obsolete now, as well
as .travis.yml and .appveyor.yml.
* common.cmake: added Clang support. In the process, a great deal has
been learned about how CMake works; in particular, static runtime
support has been reworked to be more robust.
* project: the entire notion of a "toolchain" has been reworked.
Instead of a measly --mingw parameter, there's now a separate --toolset
parameter, which allows you to choose between GCC, Clang, MSVC, etc.
Both Boost and CMake build scripts were enhanced greatly to support
Clang and other toolchains in a more robust way.
31 files changed, 2273 insertions, 163 deletions
diff --git a/.ci/verify_symbols.sh b/.ci/verify_symbols.sh index 2fe2d5d..73cc6ba 100755 --- a/.ci/verify_symbols.sh +++ b/.ci/verify_symbols.sh @@ -29,7 +29,7 @@ main() { local symbol for symbol; do - if echo "$nm" | grep -F --quiet -e " $symbol"; then + if echo "$nm" | grep -F -e " $symbol"; then echo "$script_name: file '$path' has symbol '$symbol'" else echo "$script_name: symbol '$symbol' wasn't found in file '$path'" diff --git a/.github/actions/build-boost/action.yml b/.github/actions/build-boost/action.yml new file mode 100644 index 0000000..1f8a46c --- /dev/null +++ b/.github/actions/build-boost/action.yml @@ -0,0 +1,53 @@ +name: Build Boost +description: Build Boost +inputs: + boost-dir: + description: Boost directory + required: false + toolset: + description: Toolset to use + required: false + default: auto + libraries: + description: Libraries to build + required: false + platform: + description: Target platform + required: false + default: x64 + configuration: + description: Configuration to build + required: false + default: Debug +runs: + using: composite + steps: + - run: | + $python = 'python' + + $boost_dir = '${{ inputs.boost-dir }}' + if (-not $boost_dir) { + $boost_dir = $env:BOOST_DIR + } + + $libraries = '${{ inputs.libraries }}' + $libraries = $libraries.Split(' ') | %{ "--with-$_" } + if (-not $libraries) { + Remove-Variable libraries + } + + if ($env:CI_HOST_CYGWIN) { + $python = 'python3' + $boost_dir = cygpath.exe -ua $boost_dir + } + + & $python ` + -m project.boost.build ` + --verbose ` + --toolset '${{ inputs.toolset }}' ` + --platform '${{ inputs.platform }}' ` + --configuration '${{ inputs.configuration }}' ` + -- ` + $boost_dir ` + $libraries + shell: pwsh diff --git a/.github/actions/build-example/action.yml b/.github/actions/build-example/action.yml new file mode 100644 index 0000000..97cc627 --- /dev/null +++ b/.github/actions/build-example/action.yml @@ -0,0 +1,69 @@ +name: Build example project +description: Build example project +inputs: + src-dir: + description: Project directory + required: true + boost-dir: + description: Boost directory + required: false + toolset: + description: Toolset to use + required: false + default: auto + platform: + description: Target platform + required: false + default: x64 + configuration: + description: Configuration to build + required: false + default: Debug +outputs: + install-dir: + description: Installation directory + value: '${{ steps.install-dir.outputs.path }}' +runs: + using: composite + steps: + - id: install-dir + run: | + $src_dir = Resolve-Path '${{ inputs.src-dir }}' + $project_name = Split-Path $src_dir -Leaf + $install_dir = Join-Path (Split-Path $env:GITHUB_WORKSPACE) "install_$project_name" + echo "::set-output name=path::$install_dir" + shell: pwsh + - run: | + $python = 'python' + $src_dir = '${{ inputs.src-dir }}' + $install_dir = '${{ steps.install-dir.outputs.path }}' + $boost_dir = '${{ inputs.boost-dir }}' + + if ($env:CI_HOST_CYGWIN) { + $python = 'python3' + $src_dir = cygpath.exe -ua $src_dir + $install_dir = cygpath.exe -ua $install_dir + if ($boost_dir) { + $boost_dir = cygpath.exe -ua $boost_dir + } + } + + $args = @( + '--install', + $install_dir, + '--platform', + '${{ inputs.platform }}', + '--configuration', + '${{ inputs.configuration }}', + '--toolset', + '${{ inputs.toolset }}' + ) + + if ($boost_dir) { + $args += '--boost',$boost_dir + } + + $args += '--',$src_dir + $env:VERBOSE = 1 + & $python -m project.cmake.build $args + shell: pwsh diff --git a/.github/actions/check-arch/action.yml b/.github/actions/check-arch/action.yml new file mode 100644 index 0000000..b85be4e --- /dev/null +++ b/.github/actions/check-arch/action.yml @@ -0,0 +1,23 @@ +name: Verify architecture +description: Verify target architecture +inputs: + path: + description: Installation directory + required: true + expected: + description: Target platform + required: true +runs: + using: composite + steps: + - run: | + $script_path = if ($env:CI_TARGET_PE) { + Join-Path $env:GITHUB_WORKSPACE '.ci' 'verify_arch.ps1' + } else { + Join-Path $env:GITHUB_WORKSPACE '.ci' 'verify_arch.sh' + } + + $exe_path = (Join-Path '${{ inputs.path }}' 'bin' 'foo') + $env:CI_EXE_EXT + + & $script_path $exe_path '${{ inputs.expected }}' + shell: pwsh diff --git a/.github/actions/check-boost-bootstrapped/action.yml b/.github/actions/check-boost-bootstrapped/action.yml new file mode 100644 index 0000000..73f44a9 --- /dev/null +++ b/.github/actions/check-boost-bootstrapped/action.yml @@ -0,0 +1,44 @@ +name: Check that Boost was bootstrapped +description: Check that Boost was bootstrapped +runs: + using: composite + steps: + - run: | + echo '----------------------------------------------------------------' + echo 'bootstrap.log' + echo '----------------------------------------------------------------' + $path = Join-Path $env:BOOST_DIR 'bootstrap.log' + cat $path + shell: pwsh + - run: | + echo '' + echo '----------------------------------------------------------------' + echo 'project-config.jam' + echo '----------------------------------------------------------------' + $path = Join-Path $env:BOOST_DIR 'project-config.jam' + if (Test-Path $path -Type Leaf) { + cat $path + } + shell: pwsh + - run: | + echo '' + echo '----------------------------------------------------------------' + echo 'Checking that b2 executable was built' + echo '----------------------------------------------------------------' + $name = 'b2' + $path = Join-Path $env:BOOST_DIR $name + + $exists_plain = Test-Path $path -Type Leaf + $exists_exe = Test-Path "${path}.exe" -Type Leaf + $exists = if ($env:CI_HOST_CYGWIN) { + $exists_plain -or $exists_exe + } elseif ($env:CI_HOST_WINDOWS) { + $exists_exe + } else { + $exists_plain + } + + if (-not $exists) { + throw "b2 executable wasn't found" + } + shell: pwsh diff --git a/.github/actions/check-boost-libraries/action.yml b/.github/actions/check-boost-libraries/action.yml new file mode 100644 index 0000000..88df016 --- /dev/null +++ b/.github/actions/check-boost-libraries/action.yml @@ -0,0 +1,77 @@ +name: Check that Boost libraries were built +description: Check that Boost libraries were built +inputs: + boost-dir: + description: Boost directory + required: false + libraries: + description: Libraries to check + required: true + platform: + description: Target platform + required: false + default: x64 + configuration: + description: Configuration to check + required: false + default: Debug +runs: + using: composite + steps: + - run: | + $boost_dir = '${{ inputs.boost-dir }}' + if (-not $boost_dir) { + $boost_dir = $env:BOOST_DIR + } + + $libraries = '${{ inputs.libraries }}' + $libraries = $libraries.Split(' ') + + $stagedir = Join-Path 'stage' '${{ inputs.platform }}' '${{ inputs.configuration }}' + $librarydir = Join-Path $boost_dir $stagedir 'lib' + + function Get-FileStubs { + param( + [Parameter(Mandatory=$true)] + [string] $lib + ) + + if ($lib -eq 'test') { + "prg_exec_monitor","test_exec_monitor","unit_test_framework" + } else { + @($lib) + } + } + + $files = @() + foreach ($lib in $libraries) { + $stubs = Get-FileStubs $lib + foreach ($stub in $stubs) { + $files += "${env:CI_LIB_PREFIX}boost_$stub${env:CI_LIB_EXT}" + } + } + $files = $files | Sort-Object + + echo '----------------------------------------------------------------' + echo 'Expected files' + echo '----------------------------------------------------------------' + echo $files + + echo '' + echo '----------------------------------------------------------------' + echo 'Actual files' + echo '----------------------------------------------------------------' + echo (Get-ChildItem $librarydir | Sort-Object Name).Name + + $missing = @() + + foreach ($file in $files) { + if (!(Test-Path (Join-Path $librarydir $file) -Type Leaf)) { + $missing += $file + } + } + + if ($missing.Count -ne 0) { + throw "These libraries are missing: $($missing -join ', ')" + } + shell: pwsh diff --git a/.github/actions/check-runtime-library/action.yml b/.github/actions/check-runtime-library/action.yml new file mode 100644 index 0000000..3025334 --- /dev/null +++ b/.github/actions/check-runtime-library/action.yml @@ -0,0 +1,161 @@ +name: Verify runtime library linkage +description: Verify runtime library linkage (static or shared) +inputs: + path: + description: Installation directory + required: true +runs: + using: composite + steps: + - run: | + New-Variable path -Value '${{ inputs.path }}' -Option Constant + + function Parse-DllLine { + param( + [Parameter(Mandatory=$true)] + [string] $Line + ) + + if ($Line -match '^\s*DLL Name:\s*(?<dll>\S+)\s*$') { + $Matches.dll + } else { + throw "Line doesn't match the pattern: $Line" + } + } + + function Parse-PeHeaders { + param( + [Parameter(ValueFromPipeline)] + [string[]] $Output + ) + + process { + $Output | Select-String 'DLL Name:' -SimpleMatch -CaseSensitive | %{ + Parse-DllLine $_ + } + } + } + + function Parse-NeededLine { + param( + [Parameter(Mandatory=$true)] + [string] $Line + ) + + if ($Line -match '^\s*NEEDED\s*(?<dll>\S+)\s*$') { + $Matches.dll + } else { + throw "Line doesn't match the pattern: $Line" + } + } + + function Parse-ElfHeaders { + param( + [Parameter(ValueFromPipeline)] + [string[]] $Output + ) + + process { + $Output | Select-String 'NEEDED' -SimpleMatch -CaseSensitive | %{ + Parse-NeededLine $_ + } + } + } + + function Get-LinkedLibraries { + param( + [Parameter(Mandatory=$true)] + [string] $ExePath + ) + + $objdump = 'objdump' + if ($env:CI_HOST_WINDOWS) { + $objdump = 'C:\ProgramData\chocolatey\bin\objdump.exe' + } + + if ($env:CI_TARGET_PE) { + & $objdump -x $ExePath | Parse-PeHeaders | echo + } + if ($env:CI_TARGET_ELF) { + & $objdump -x $ExePath | Parse-ElfHeaders | echo + } + } + + function Do-ValidateLinkedLibraries { + param( + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [string[]] $Actual, + + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [string[]] $Required, + + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [string[]] $Optional + ) + + echo 'Linked libraries:' + echo $Actual + + $missing = $Required | ?{$_ -notin $Actual} + + if ($missing.Count -gt 0) { + throw "Doesn't link to the following libraries: $($missing -join ', ')" + } + + $unexpected = $Actual | ?{$_ -notin $Required -and $_ -notin $Optional} + + if ($unexpected.Count -gt 0) { + throw "Links to the following unexpected libraries: $($unexpected -join ', ')" + } + } + + function Validate-LinkedLibraries { + param( + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [string[]] $Actual + ) + + $windows_required = @('KERNEL32.dll') + if ($env:CI_MINGW) { + $windows_required += 'msvcrt.dll' + } + # Linking libstdc++ statically on Cygwin is broken, see the + # cygwin_static_libstdc++.yml workflow. + $cygwin_required = $windows_required + @('cygwin1.dll','cygstdc++-6.dll') + $linux_required = @('libc.so.6') + + $windows_optional = @('baz.dll') + if ($env:CI_MINGW) { + $windows_optional = @('libbaz.dll','USER32.dll') + } + $cygwin_optional = @('cygbaz.dll') + $linux_optional = @('libbaz.so','ld-linux.so.2','ld-linux-x86-64.so.2','libm.so.6') + + if ($env:CI_TARGET_CYGWIN) { + Do-ValidateLinkedLibraries ` + -Actual $Actual ` + -Required $cygwin_required ` + -Optional $cygwin_optional + } elseif ($env:CI_TARGET_WINDOWS) { + Do-ValidateLinkedLibraries ` + -Actual $Actual ` + -Required $windows_required ` + -Optional $windows_optional + } elseif ($env:CI_TARGET_LINUX) { + Do-ValidateLinkedLibraries ` + -Actual $Actual ` + -Required $linux_required ` + -Optional $linux_optional + } else { + throw 'Where am I?' + } + } + + $exe_path = (Join-Path $path 'bin' 'foo') + $env:CI_EXE_EXT + $libraries = Get-LinkedLibraries $exe_path + Validate-LinkedLibraries $libraries + shell: pwsh diff --git a/.github/actions/check-symbols/action.yml b/.github/actions/check-symbols/action.yml new file mode 100644 index 0000000..7428be3 --- /dev/null +++ b/.github/actions/check-symbols/action.yml @@ -0,0 +1,46 @@ +name: Check symbols +description: Check debug symbols +inputs: + path: + description: Installation directory + required: true + expected: + description: Expected symbols + required: true +runs: + using: composite + steps: + - run: | + $install_dir = '${{ inputs.path }}' + $bin_dir = Join-Path $install_dir 'bin' + $lib_dir = Join-Path $install_dir 'lib' + + $script_path = if ($env:CI_TARGET_ELF) { + Join-Path $env:GITHUB_WORKSPACE '.ci' 'verify_symbols.sh' + } else { + throw "Verifying symbols for PE executables is not implemented" + } + + ConvertFrom-Json '${{ inputs.expected }}' | %{ + $target = $_.target + $type = $_.type + + switch -Exact ($type) { + 'exe' { + $file = $target + $env:CI_EXE_EXT + $path = Join-Path $bin_dir $file + } + 'dll' { + $file = $env:CI_DLL_PREFIX + $target + $env:CI_DLL_EXT + $path = if ($env:CI_DLL_IN_BIN) { + Join-Path $bin_dir $file + } else { + Join-Path $lib_dir $file + } + } + default { throw "Unrecognized type: $type" } + } + + & $script_path $path $_.symbols + } + shell: pwsh diff --git a/.github/actions/common-variables/action.yml b/.github/actions/common-variables/action.yml new file mode 100644 index 0000000..3d40cdd --- /dev/null +++ b/.github/actions/common-variables/action.yml @@ -0,0 +1,85 @@ +name: Set common variables +description: Set common run variables +inputs: + toolset: + description: Toolset used + required: true + cygwin: + description: Targeting Cygwin + required: false + default: 0 +runs: + using: composite + steps: + - run: | + New-Variable toolset -Value '${{ inputs.toolset }}' -Option Constant + New-Variable cygwin -Value ('${{ inputs.cygwin }}' -eq '1') -Option Constant + + New-Variable os -Value '${{ runner.os }}' -Option Constant + + New-Variable host_linux -Value ($os -eq 'Linux') -Option Constant + New-Variable host_cygwin -Value $cygwin -Option Constant + New-Variable host_windows -Value ($os -eq 'Windows' -and !$host_cygwin) -Option Constant + + New-Variable mingw -Value ($toolset -eq 'mingw' -or ($host_windows -and $toolset -eq 'gcc')) -Option Constant + + New-Variable target_linux -Value ($host_linux -and !$mingw) -Option Constant + New-Variable target_cygwin -Value ($host_cygwin -and !$mingw) -Option Constant + New-Variable target_windows -Value ($mingw -or $host_windows) -Option Constant + + New-Variable target_pe -Value ($target_windows -or $target_cygwin) -Option Constant + New-Variable target_elf -Value $target_linux -Option Constant + + function Set-BoolVar { + param( + [Parameter(Mandatory=$true)] + [string] $name, + [Parameter(Mandatory=$true)] + [bool] $value + ) + + $str_value = if ($value) { '1' } else { '' } + $msg = "$name=$str_value" + echo $msg + echo $msg >> $env:GITHUB_ENV + } + + function Set-StrVar { + param( + [Parameter(Mandatory=$true)] + [string] $name, + [string] $value + ) + + $msg = "$name=$value" + echo $msg + echo $msg >> $env:GITHUB_ENV + } + + Set-BoolVar 'CI_HOST_LINUX' $host_linux + Set-BoolVar 'CI_HOST_CYGWIN' $host_cygwin + Set-BoolVar 'CI_HOST_WINDOWS' $host_windows + Set-BoolVar 'CI_MINGW' $mingw + Set-BoolVar 'CI_TARGET_LINUX' $target_linux + Set-BoolVar 'CI_TARGET_CYGWIN' $target_cygwin + Set-BoolVar 'CI_TARGET_WINDOWS' $target_windows + + Set-BoolVar 'CI_TARGET_PE' $target_pe + Set-BoolVar 'CI_TARGET_ELF' $target_elf + + $lib_prefix = 'lib' + $lib_ext = if ($target_windows -and -not $mingw) { '.lib' } else { '.a' } + + $dll_prefix = if ($target_windows -and -not $mingw) { '' } elseif ($target_cygwin) { 'cyg' } else { 'lib' } + $dll_ext = if ($target_windows -or $target_cygwin) { '.dll' } else { '.so' } + + $exe_ext = if ($target_pe) { '.exe' } else { '' } + + Set-StrVar 'CI_LIB_PREFIX' $lib_prefix + Set-StrVar 'CI_LIB_EXT' $lib_ext + Set-StrVar 'CI_DLL_PREFIX' $dll_prefix + Set-StrVar 'CI_DLL_EXT' $dll_ext + Set-StrVar 'CI_EXE_EXT' $exe_ext + + Set-BoolVar 'CI_DLL_IN_BIN' $target_pe + shell: pwsh diff --git a/.github/actions/download-boost/action.yml b/.github/actions/download-boost/action.yml new file mode 100644 index 0000000..fc0be88 --- /dev/null +++ b/.github/actions/download-boost/action.yml @@ -0,0 +1,40 @@ +name: Download Boost +description: Download & unpack Boost +inputs: + boost-version: + description: 'Boost version' + required: true +runs: + using: composite + steps: + - run: echo 'BOOST_VERSION=${{ inputs.boost-version }}' >> $env:GITHUB_ENV + shell: pwsh + - run: | + $parent_dir = Split-Path $env:GITHUB_WORKSPACE + $unpack_dir = Join-Path $parent_dir 'boost' + echo "BOOST_UNPACK_DIR=$unpack_dir" >> $env:GITHUB_ENV + shell: pwsh + - run: | + $dir_name = "boost_$($env:BOOST_VERSION.Replace('.', '_'))" + $dir = Join-Path $env:BOOST_UNPACK_DIR $dir_name + echo "BOOST_DIR=$dir" >> $env:GITHUB_ENV + shell: pwsh + - run: | + echo '' + echo '----------------------------------------------------------------' + echo "BOOST_VERSION: $env:BOOST_VERSION" + echo "BOOST_UNPACK_DIR: $env:BOOST_UNPACK_DIR" + echo "BOOST_DIR: $env:BOOST_DIR" + echo '----------------------------------------------------------------' + New-Item $env:BOOST_UNPACK_DIR -Type Directory | Out-Null + + $python = 'python' + $unpack_dir = $env:BOOST_UNPACK_DIR + + if ($env:CI_HOST_CYGWIN) { + $python = 'python3' + $unpack_dir = cygpath.exe -ua $unpack_dir + } + + & $python -m project.boost.download --unpack $unpack_dir -- $env:BOOST_VERSION + shell: pwsh diff --git a/.github/actions/run-example-boost/action.yml b/.github/actions/run-example-boost/action.yml new file mode 100644 index 0000000..0dd9f44 --- /dev/null +++ b/.github/actions/run-example-boost/action.yml @@ -0,0 +1,45 @@ +name: Run examples/boost +description: Run examples/boost +inputs: + path: + description: Installation directory + required: true +runs: + using: composite + steps: + - run: | + New-Variable path -Value '${{ inputs.path }}' -Option Constant + + if ($env:CI_HOST_LINUX -and -not $env:CI_TARGET_LINUX) { + echo 'Not going to do that on Linux/MinGW' + exit + } + + $relative_test = 'test.txt' + $absolute_test = Join-Path (Get-Location).Path 'test.txt' + + $exe_path = (Join-Path $path 'bin' 'foo') + if (-not $env:CI_TARGET_CYGWIN) { + $exe_path += $env:CI_EXE_EXT + } + + $argv0 = $exe_path + if ($env:CI_TARGET_CYGWIN) { + # Apparently, Cygwin programs convert argv[0] when executing native + # programs or being executed by them. + $argv0 = cygpath.exe -ua $argv0 + } + + $actual = & $exe_path $relative_test + + echo 'Actual output:' + echo $actual + + $expected = $argv0,$absolute_test + echo 'Expected output:' + echo $expected + + if (Compare-Object $actual $expected -CaseSensitive) { + throw 'Unexpected output' + } + shell: pwsh diff --git a/.github/actions/run-example/action.yml b/.github/actions/run-example/action.yml new file mode 100644 index 0000000..5300ab9 --- /dev/null +++ b/.github/actions/run-example/action.yml @@ -0,0 +1,28 @@ +name: Run example project +description: Run example project +inputs: + path: + description: Installation directory + required: true +runs: + using: composite + steps: + - run: | + New-Variable path -Value '${{ inputs.path }}' -Option Constant + + if ($env:CI_HOST_LINUX -and -not $env:CI_TARGET_LINUX) { + echo 'Not going to do that on Linux/MinGW' + exit + } + + if ($env:CI_TARGET_LINUX) { + $lib = Join-Path $path 'lib' + if (Test-Path $lib) { + $lib = Resolve-Path $lib + $env:LD_LIBRARY_PATH = $lib + } + } + $exe_path = (Join-Path $path 'bin' 'foo') + $env:CI_EXE_EXT + echo "Executable path: $exe_path" + & $exe_path + shell: pwsh diff --git a/.github/actions/software-environment/action.yml b/.github/actions/software-environment/action.yml new file mode 100644 index 0000000..c8a416b --- /dev/null +++ b/.github/actions/software-environment/action.yml @@ -0,0 +1,63 @@ +name: Set up software environment +description: Set up software paths and versions +inputs: + toolset: + description: Toolset to use + required: true + platform: + description: Target platform + required: false + default: x64 +runs: + using: composite + steps: + - run: | + if ($env:CI_HOST_WINDOWS) { + echo 'C:\Program Files\CMake\bin' >> $env:GITHUB_PATH + } + shell: pwsh + - run: | + echo '------------------------------------------------------------' + echo 'PATH' + echo '------------------------------------------------------------' + echo $env:PATH.Split([IO.Path]::PathSeparator) + shell: pwsh + - run: | + function Print-Info { + param( + [Parameter(Mandatory=$true)] + [string] $name, + [Parameter(Mandatory=$true)] + [string] $exe + ) + + echo '' + echo '------------------------------------------------------------' + echo $name + echo '------------------------------------------------------------' + $full_path = Get-Command $exe -ErrorAction SilentlyContinue + if ($full_path) { + $full_path = $full_path.Path + echo "Location: $full_path" + echo 'Version info:' + & $full_path --version + } else { + echo "Executable '$name' couldn't be found!" + } + } + + $python = 'python' + if ($env:CI_HOST_CYGWIN) { + $python = 'python3' + } + + Print-Info 'python' $python + Print-Info 'cmake' 'cmake' + Print-Info 'make' 'make' + Print-Info 'mingw32-make' 'mingw32-make' + Print-Info 'g++' 'g++' + Print-Info 'clang++' 'clang++' + Print-Info 'clang-cl' 'clang-cl' + Print-Info 'i686-w64-mingw32-g++' 'i686-w64-mingw32-g++' + Print-Info 'x86_64-w64-mingw32-g++' 'x86_64-w64-mingw32-g++' + shell: pwsh diff --git a/.github/workflows/boost_clang_windows.yml b/.github/workflows/boost_clang_windows.yml new file mode 100644 index 0000000..62c2cb5 --- /dev/null +++ b/.github/workflows/boost_clang_windows.yml @@ -0,0 +1,143 @@ +# This basically tests two things. +# +# * If, instead of some kind of MinGW-born ar & ranlib, you only have +# upstream LLVM distribution on Windows, you wouldn't be able to use +# toolset=clang until 1.66.0. +# * toolset=clang-win is broken until 1.69.0. + +name: Boost & Clang on Windows + +on: + push: + pull_request: + schedule: + # Weekly, at 5:30 AM on Saturday (somewhat randomly chosen). + - cron: '30 5 * * 6' + workflow_dispatch: + +jobs: + build: + runs-on: windows-2019 + + strategy: + fail-fast: false + matrix: + toolset: [clang, clang-cl] + boost-version: [1.63.0, 1.64.0, 1.65.1, 1.66.0, 1.67.0, 1.68.0, 1.69.0, 1.70.0, 1.71.0, 1.74.0] + include: + - {toolset: clang, b2_toolset: clang} + - {toolset: clang-cl, b2_toolset: clang-win} + + name: '${{ matrix.toolset }} / ${{ matrix.boost-version }}' + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Clean up PATH + uses: egor-tensin/cleanup-path@v1 + if: runner.os == 'Windows' + + - name: Set common variables + uses: ./.github/actions/common-variables + with: + toolset: '${{ matrix.toolset }}' + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install Clang + uses: egor-tensin/setup-clang@v1 + + - name: Set up software environment + uses: ./.github/actions/software-environment + with: + toolset: ${{ matrix.toolset }} + + - name: Download Boost + uses: ./.github/actions/download-boost + with: + boost-version: ${{ matrix.boost-version }} + + - name: Bootstrap Boost + run: | + cd $env:BOOST_DIR + .\bootstrap.bat + continue-on-error: true + + - name: Check that Boost was bootstrapped + uses: ./.github/actions/check-boost-bootstrapped + + - name: Write toolset-config.jam (clang) + run: | + echo 'using ${{ matrix.b2_toolset }} : : clang++.exe : <archiver>llvm-ar <ranlib>llvm-ranlib ;' > "$env:BOOST_DIR\toolset-config.jam" + if: matrix.toolset == 'clang' + - name: Write toolset-config.jam (clang-cl) + run: | + echo 'using ${{ matrix.b2_toolset }} ;' > "$env:BOOST_DIR\toolset-config.jam" + if: matrix.toolset == 'clang-cl' + + - name: Build Boost.Filesystem + run: | + cd $env:BOOST_DIR + + $stagedir = "stage" + $librarydir = "$env:BOOST_DIR\$stagedir\lib" + echo "BOOST_LIBRARYDIR=$librarydir" >> $env:GITHUB_ENV + + .\b2.exe ` + "--stagedir=$stagedir" ` + --layout=system ` + --dump-configuration ` + address-model=64 ` + "--user-config=$env:BOOST_DIR\toolset-config.jam" ` + variant=debug ` + link=static ` + runtime-link=static ` + -d2 --dump-configuration ` + --with-filesystem ` + --with-system + continue-on-error: true + + - name: Boost.Filesystem failed to build + run: $(Test-Path "$env:BOOST_LIBRARYDIR\libboost_filesystem.lib" -Type Leaf) -and $(throw "libboost_filesystem.lib was build?!") + if: | + (matrix.toolset == 'clang' && matrix.boost-version < '1.66.0') || (matrix.toolset == 'clang-cl' && matrix.boost-version < '1.69.0') + - id: boost_filesystem_built + name: Boost.Filesystem was built + run: $(Test-Path "$env:BOOST_LIBRARYDIR\libboost_filesystem.lib" -Type Leaf) -or $(throw "libboost_filesystem.lib wasn't found") + if: | + !((matrix.toolset == 'clang' && matrix.boost-version < '1.66.0') || (matrix.toolset == 'clang-cl' && matrix.boost-version < '1.69.0')) + + # Check that we can link to the built libraries. + - name: Build foo.exe using clang + run: | + clang++.exe ` + "-I$env:BOOST_DIR" ` + -D BOOST_ALL_NO_LIB=1 ` + "-L$env:BOOST_LIBRARYDIR" ` + -llibboost_filesystem ` + -llibboost_system ` + -o foo.exe ` + examples/boost/foo.cpp + if: steps.boost_filesystem_built.conclusion == 'success' && matrix.toolset == 'clang' + + - name: Build foo.exe using clang-cl + run: | + clang-cl.exe ` + /EHsc ` + /MTd ` + "/I$env:BOOST_DIR" ` + /D BOOST_ALL_NO_LIB=1 ` + "/Fefoo.exe" ` + examples/boost/foo.cpp ` + libboost_filesystem.lib ` + libboost_system.lib ` + /link "/LIBPATH:$env:BOOST_LIBRARYDIR" + if: steps.boost_filesystem_built.conclusion == 'success' && matrix.toolset == 'clang-cl' + + - name: foo.exe + run: .\foo.exe + if: steps.boost_filesystem_built.conclusion == 'success' diff --git a/.github/workflows/boost_toolsets.yml b/.github/workflows/boost_toolsets.yml new file mode 100644 index 0000000..1319264 --- /dev/null +++ b/.github/workflows/boost_toolsets.yml @@ -0,0 +1,222 @@ +name: Boost (toolsets) + +on: + push: + pull_request: + schedule: + # Weekly, at 5:30 AM on Saturday (somewhat randomly chosen). + - cron: '30 5 * * 6' + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + boost-version: [1.58.0, 1.65.0, 1.72.0] + toolset: [auto, clang, clang-cl, gcc, mingw, msvc] + cygwin: [0, 1] + os: [ubuntu-18.04, windows-2016, windows-2019] + + include: + # Prettier run names. + - {os: windows-2019, name: VS 2019} + - {os: windows-2016, name: VS 2017} + - {os: ubuntu-18.04, name: Ubuntu} + - {cygwin: 1, name: Cygwin} + # Target platform. + - {boost-version: 1.58.0, platform: x64} + - {boost-version: 1.65.0, platform: x86} + - {boost-version: 1.72.0, platform: x64} + # Configuration. + - {boost-version: 1.58.0, configuration: Debug} + - {boost-version: 1.65.0, configuration: MinSizeRel} + - {boost-version: 1.72.0, configuration: Release} + + # Some Boost libraries commonly used by me. + - libraries: filesystem program_options regex system test + # On Windows, clang fails to build Boost.Test prior to version 1.61 + # with the following error: + # + # .\boost/test/impl/execution_monitor.ipp:1134:20: error: cannot compile this 'this' captured by SEH yet + # + # This was fixed for 1.61 in this commit: + # https://github.com/boostorg/test/commit/c94ef6982e2ebe77f9376579547c228f0d62e45f. + # On Linux/Cygwin, everything should be fine though. + - toolset: clang + boost-version: 1.58.0 + os: windows-2019 + libraries: filesystem program_options regex system + - toolset: clang + boost-version: 1.58.0 + os: windows-2016 + libraries: filesystem program_options regex system + exclude: + # Ubuntu: no MSVC/clang-cl/Cygwin. + - {os: ubuntu-18.04, toolset: msvc} + - {os: ubuntu-18.04, toolset: clang-cl} + - {os: ubuntu-18.04, cygwin: 1} + # Cygwin: no MSVC/clang-cl. + - {cygwin: 1, toolset: msvc} + - {cygwin: 1, toolset: clang-cl} + # Cygwin is the same on Windows Server 2016 & 2019. + - {os: windows-2016, cygwin: 1} + # clang-cl is only supported by Boost.Build since 1.69 (see the + # boost_clang_windows.yml workflow). + - {toolset: clang-cl, boost-version: 1.58.0} + - {toolset: clang-cl, boost-version: 1.65.0} + + runs-on: '${{ matrix.os }}' + + name: '${{ matrix.boost-version }} / ${{ matrix.toolset }} / ${{ matrix.name }}' + + # 1) I have no idea why, but GCC 10.2 fails to build Boost.Filesystem with + # the following errors: + # + # libs\filesystem\src\path.cpp:36:11: fatal error: windows_file_codecvt.hpp: No such file or directory + # 36 | # include "windows_file_codecvt.hpp" + # | ^~~~~~~~~~~~~~~~~~~~~~~~~~ + # compilation terminated. + # libs\filesystem\src\windows_file_codecvt.cpp:25:10: fatal error: windows_file_codecvt.hpp: No such file or directory + # 25 | #include "windows_file_codecvt.hpp" + # | ^~~~~~~~~~~~~~~~~~~~~~~~~~ + # compilation terminated. + # + # This seems to be a compiler bug, since the file is _definitely_ there, + # and Clang 8.0.1 builds it successfully. This only applies to Boost + # versions up to and including 1.60.0 for some reason. I can easily + # reproduce this locally. TODO: remove when GCC on Cygwin is treated + # with a bugfix to this? + # + # 2) 32-bit Cygwin fucking sucks. In many ways actually, but the real + # reason was the incomprehensible + # + # undefined reference to `__chkstk_ms' + # + # error when building with Clang. + continue-on-error: ${{ + (matrix.cygwin == '1' && matrix.boost-version == '1.58.0' + && (matrix.toolset == 'auto' + || matrix.toolset == 'gcc' + || matrix.toolset == 'mingw')) + || (matrix.cygwin == '1' && matrix.platform == 'x86') + }} + + defaults: + run: + shell: pwsh + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Clean up PATH + uses: egor-tensin/cleanup-path@v1 + if: runner.os == 'Windows' + + - name: Set common variables + uses: ./.github/actions/common-variables + with: + toolset: '${{ matrix.toolset }}' + cygwin: '${{ matrix.cygwin }}' + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + if: '!env.CI_HOST_CYGWIN' + + - name: Install Cygwin + uses: egor-tensin/setup-cygwin@v3 + with: + platform: '${{ matrix.platform }}' + packages: cmake make python3 + hardlinks: 1 + if: env.CI_HOST_CYGWIN + + - name: Install GCC + uses: egor-tensin/setup-gcc@v1 + with: + cygwin: '${{ matrix.cygwin }}' + platform: '${{ matrix.platform }}' + hardlinks: 1 + if: env.CI_HOST_CYGWIN || (env.CI_HOST_LINUX && (matrix.toolset == 'auto' || matrix.toolset == 'gcc')) + + - name: Install Clang + uses: egor-tensin/setup-clang@v1 + with: + cygwin: '${{ matrix.cygwin }}' + platform: '${{ matrix.platform }}' + hardlinks: 1 + if: matrix.toolset == 'clang' || matrix.toolset == 'clang-cl' + + - name: Install MinGW + uses: egor-tensin/setup-mingw@v2 + with: + cygwin: '${{ matrix.cygwin }}' + platform: '${{ matrix.platform }}' + hardlinks: 1 + # toolset == 'clang' needs some kind of make, e.g. mingw32-make: + if: env.CI_MINGW || (matrix.toolset == 'clang' && env.CI_HOST_WINDOWS) + + - name: Set up software environment + uses: ./.github/actions/software-environment + with: + toolset: '${{ matrix.toolset }}' + platform: '${{ matrix.platform }}' + + - name: Set up Visual Studio + uses: egor-tensin/vs-shell@v2 + with: + arch: '${{ matrix.platform }}' + if: matrix.toolset == 'clang-cl' && env.CI_HOST_WINDOWS + + - name: Download Boost + uses: ./.github/actions/download-boost + with: + boost-version: '${{ matrix.boost-version }}' + + - name: Build Boost + uses: ./.github/actions/build-boost + with: + toolset: '${{ matrix.toolset }}' + libraries: '${{ matrix.libraries }}' + platform: '${{ matrix.platform }}' + configuration: '${{ matrix.configuration }}' + continue-on-error: true + + - name: Check that Boost was bootstrapped + uses: ./.github/actions/check-boost-bootstrapped + + - name: Check that Boost libraries were built + uses: ./.github/actions/check-boost-libraries + with: + libraries: '${{ matrix.libraries }}' + platform: '${{ matrix.platform }}' + configuration: '${{ matrix.configuration }}' + + - name: Build examples/boost + id: build_example + uses: ./.github/actions/build-example + with: + src-dir: examples/boost + boost-dir: '${{ env.BOOST_DIR }}' + toolset: '${{ matrix.toolset }}' + platform: '${{ matrix.platform }}' + configuration: '${{ matrix.configuration }}' + + - name: Verify runtime library linkage + uses: ./.github/actions/check-runtime-library + with: + path: '${{ steps.build_example.outputs.install-dir }}' + + - name: Verify architecture + uses: ./.github/actions/check-arch + with: + path: '${{ steps.build_example.outputs.install-dir }}' + expected: '${{ matrix.platform }}' + + - name: Run examples/boost + uses: ./.github/actions/run-example-boost + with: + path: '${{ steps.build_example.outputs.install-dir }}' diff --git a/.github/workflows/cygwin_static_libstdc++.yml b/.github/workflows/cygwin_static_libstdc++.yml new file mode 100644 index 0000000..156b111 --- /dev/null +++ b/.github/workflows/cygwin_static_libstdc++.yml @@ -0,0 +1,81 @@ +# -static-libstdc++ is broken on Cygwin for some reason, as this workflow tries +# to demonstrate. I don't know why exactly, but I'm not the only one with this +# problem: +# +# * https://stackoverflow.com/q/46854365/514684 +# * https://sourceforge.net/p/stlplus/discussion/345536/thread/48c7fc9c17/?limit=25 + +name: Cygwin & -static-libstdc++ + +on: + push: + pull_request: + schedule: + # Weekly, at 5:30 AM on Saturday (somewhat randomly chosen). + - cron: '30 5 * * 6' + workflow_dispatch: + +jobs: + test: + runs-on: windows-2019 + + name: Test + + defaults: + run: + shell: pwsh + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Clean up PATH + uses: egor-tensin/cleanup-path@v1 + if: runner.os == 'Windows' + + - name: Set common variables + uses: ./.github/actions/common-variables + with: + toolset: '${{ matrix.toolset }}' + cygwin: 1 + + - name: Install Cygwin + uses: egor-tensin/setup-cygwin@v3 + + - name: Install GCC + uses: egor-tensin/setup-gcc@v1 + with: + cygwin: 1 + + - name: Set up software environment + uses: ./.github/actions/software-environment + with: + toolset: gcc + + - name: test.cpp + run: | + $src = @" + #include <stdexcept> + + int main() { + std::runtime_error x{"x"}; + std::runtime_error y{x}; + return 0; + } + "@ + + echo $src > test.cpp + + - name: Build w/ -static-libstdc++ + run: C:\tools\cygwin\bin\g++.exe -static-libstdc++ -o test test.cpp + continue-on-error: true + + - name: Should fail + run: $(Test-Path test.exe -Type Leaf) -and $(throw "test.exe shouldn't have been built") + + - name: Build w/ --allow-multiple-definition + run: C:\tools\cygwin\bin\g++.exe '-Wl,--allow-multiple-definition' -static-libstdc++ -o test test.cpp + continue-on-error: true + + - name: Should succeed + run: $(Test-Path test.exe -Type Leaf) -or $(throw "test.exe should have been built") diff --git a/.github/workflows/example_toolsets.yml b/.github/workflows/example_toolsets.yml new file mode 100644 index 0000000..cb336aa --- /dev/null +++ b/.github/workflows/example_toolsets.yml @@ -0,0 +1,168 @@ +name: Examples (toolsets) + +on: + push: + pull_request: + schedule: + # Weekly, at 5:30 AM on Saturday (somewhat randomly chosen). + - cron: '30 5 * * 6' + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + example: [simple, static, dynamic] + toolset: [auto, clang, clang-cl, gcc, mingw, msvc] + cygwin: [0, 1] + os: [ubuntu-18.04, windows-2016, windows-2019] + + include: + # Prettier run names. + - {os: windows-2019, name: VS 2019} + - {os: windows-2016, name: VS 2017} + - {os: ubuntu-18.04, name: Ubuntu} + - {cygwin: 1, name: Cygwin} + # Target platform. + - {example: simple, platform: x64} + - {example: static, platform: x86} + - {example: dynamic, platform: x64} + # Configuration. + - {example: simple, configuration: Release} + - {example: static, configuration: Debug} + - {example: dynamic, configuration: RelWithDebInfo} + # Expected symbols. + - example: simple + symbols: + - target: foo + type: exe + symbols: [] + - example: static + symbols: + - target: foo + type: exe + symbols: [main, bar] + - example: dynamic + symbols: + - target: foo + type: exe + symbols: [main] + - target: baz + type: dll + symbols: [baz] + exclude: + # Ubuntu: no MSVC/clang-cl/Cygwin. + - {os: ubuntu-18.04, toolset: msvc} + - {os: ubuntu-18.04, toolset: clang-cl} + - {os: ubuntu-18.04, cygwin: 1} + # Cygwin: no MSVC/clang-cl. + - {cygwin: 1, toolset: msvc} + - {cygwin: 1, toolset: clang-cl} + # Cygwin is the same on Windows Server 2016 & 2019. + - {os: windows-2016, cygwin: 1} + + runs-on: '${{ matrix.os }}' + + name: '${{ matrix.example }} / ${{ matrix.toolset }} / ${{ matrix.name }}' + + defaults: + run: + shell: pwsh + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Clean up PATH + uses: egor-tensin/cleanup-path@v1 + if: runner.os == 'Windows' + + - name: Set common variables + uses: ./.github/actions/common-variables + with: + toolset: '${{ matrix.toolset }}' + cygwin: '${{ matrix.cygwin }}' + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + if: '!env.CI_HOST_CYGWIN' + + - name: Install Cygwin + uses: egor-tensin/setup-cygwin@v3 + with: + platform: '${{ matrix.platform }}' + packages: cmake make python3 + hardlinks: 1 + if: env.CI_HOST_CYGWIN + + - name: Install GCC + uses: egor-tensin/setup-gcc@v1 + with: + cygwin: '${{ matrix.cygwin }}' + platform: '${{ matrix.platform }}' + hardlinks: 1 + if: (env.CI_HOST_LINUX || env.CI_HOST_CYGWIN) && (matrix.toolset == 'auto' || matrix.toolset == 'gcc') + + - name: Install Clang + uses: egor-tensin/setup-clang@v1 + with: + cygwin: '${{ matrix.cygwin }}' + platform: '${{ matrix.platform }}' + hardlinks: 1 + if: matrix.toolset == 'clang' || matrix.toolset == 'clang-cl' + + - name: Install MinGW + uses: egor-tensin/setup-mingw@v2 + with: + cygwin: '${{ matrix.cygwin }}' + platform: '${{ matrix.platform }}' + hardlinks: 1 + # toolset == 'clang' needs some kind of make, e.g. mingw32-make: + if: env.CI_MINGW || (matrix.toolset == 'clang' && env.CI_HOST_WINDOWS) + + - name: Set up software environment + uses: ./.github/actions/software-environment + with: + toolset: '${{ matrix.toolset }}' + platform: '${{ matrix.platform }}' + + - name: Set up Visual Studio + uses: egor-tensin/vs-shell@v2 + with: + arch: '${{ matrix.platform }}' + if: matrix.toolset == 'clang-cl' && env.CI_HOST_WINDOWS + + - name: Build example project + id: build + uses: ./.github/actions/build-example + with: + src-dir: 'examples/${{ matrix.example }}' + toolset: '${{ matrix.toolset }}' + platform: '${{ matrix.platform }}' + configuration: '${{ matrix.configuration }}' + + - name: Verify runtime library linkage + uses: ./.github/actions/check-runtime-library + with: + path: '${{ steps.build.outputs.install-dir }}' + + - name: Verify architecture + uses: ./.github/actions/check-arch + with: + path: '${{ steps.build.outputs.install-dir }}' + expected: '${{ matrix.platform }}' + + - name: Run example project + uses: ./.github/actions/run-example + with: + path: '${{ steps.build.outputs.install-dir }}' + + - name: Check symbols + uses: ./.github/actions/check-symbols + with: + path: '${{ steps.build.outputs.install-dir }}' + expected: '${{ toJson(matrix.symbols) }}' + if: env.CI_TARGET_ELF diff --git a/common.cmake b/common.cmake index 546db7d..f718dbf 100644 --- a/common.cmake +++ b/common.cmake @@ -23,7 +23,8 @@ if(NOT POLICY CMP0054) endif() cmake_policy(SET CMP0054 NEW) -# Toolset identification: +# Toolset identification +# ---------------------- if(CMAKE_C_COMPILER_ID) set(toolset "${CMAKE_C_COMPILER_ID}") @@ -37,11 +38,14 @@ if(toolset STREQUAL "GNU") set(is_gcc ON) elseif(toolset STREQUAL "MSVC") set(is_msvc ON) +elseif(toolset STREQUAL "Clang") + set(is_clang ON) else() message(WARNING "common.cmake: Unrecognized toolset: ${toolset}") endif() -# User-defined switches: +# User-defined switches +# --------------------- set(default_value ON) get_directory_property(parent_dir PARENT_DIRECTORY) @@ -63,10 +67,6 @@ if(NOT DEFINED CC_STATIC_RUNTIME) if(DEFINED Boost_USE_STATIC_LIBS AND NOT Boost_USE_STATIC_LIBS) # Linking to dynamic Boost libs and the static runtime is a no-no: set(static_runtime_default_value OFF) - elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows" OR MINGW) - else() - # At this point, Linux-like environment & the GNU C Library are assumed. - set(static_runtime_default_value OFF) endif() option(CC_STATIC_RUNTIME "Link the runtime statically" "${static_runtime_default_value}") endif() @@ -87,13 +87,15 @@ if(NOT parent_dir) message(STATUS "common.cmake: Strip symbols: ${CC_STRIP_SYMBOLS}") endif() -# C++ standard version: +# C++ standard +# ------------ set(CMAKE_CXX_STANDARD "${CC_CXX_STANDARD}") set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -# Common compiler options: +# Common compiler options +# ----------------------- function(_cc_best_practices_msvc target) set(compile_options /MP /W4) @@ -118,7 +120,8 @@ function(_cc_best_practices target) endif() endfunction() -# Useful Windows macros: +# Useful Windows macros +# --------------------- function(_cc_common_windows_definitions target) set(compile_definitions WIN32_LEAN_AND_MEAN NOMINMAX) @@ -135,12 +138,70 @@ function(_cc_common_windows_definitions target) endif() endfunction() -# Static runtime: +# Static runtime +# -------------- + +function(_cc_join output glue) + set(tmp "") + set(this_glue "") + foreach(arg ${ARGN}) + set(tmp "${tmp}${this_glue}${arg}") + set(this_glue "${glue}") + endforeach() + set("${output}" "${tmp}" PARENT_SCOPE) +endfunction() + +function(_cc_replace_flags str sub) + # Whenever this is used, it fucking sucks, but was tested on at least some + # CMake version. + set(flags_list + CMAKE_CXX_FLAGS + CMAKE_CXX_FLAGS_DEBUG + CMAKE_CXX_FLAGS_RELWITHDEBINFO + CMAKE_CXX_FLAGS_RELEASE + CMAKE_CXX_FLAGS_MINSIZEREL + CMAKE_C_FLAGS + CMAKE_C_FLAGS_DEBUG + CMAKE_C_FLAGS_RELWITHDEBINFO + CMAKE_C_FLAGS_RELEASE + CMAKE_C_FLAGS_MINSIZEREL) + foreach(flags ${flags_list}) + if(NOT ${flags}) + continue() + endif() + set(value "${${flags}}") + string(REPLACE "${str}" "${sub}" value "${value}") + get_property(original_docstring CACHE ${flags} PROPERTY HELPSTRING) + set(${flags} "${value}" CACHE STRING "${original_docstring}" FORCE) + endforeach() +endfunction() + +# MSVC_RUNTIME_LIBRARY is a convenient way to select the runtime library, but +# it's only available starting from 3.15. +# Additionally, it has to be enabled outside of this file (either via +# cmake_policy or setting the cmake_minimum_required to the appropriate value). + +if(POLICY CMP0091) + cmake_policy(GET CMP0091 msvc_runtime_policy) + # Use a variable as an indicator that the policy is in effect. + if(msvc_runtime_policy STREQUAL "NEW") + set(msvc_runtime_policy ON) + else() + unset(msvc_runtime_policy) + endif() +endif() + +function(_cc_static_runtime_via_policy target) + set_property(TARGET "${target}" PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>") +endfunction() function(_cc_static_runtime_msvc target) - target_compile_options("${target}" PRIVATE - $<$<CONFIG:Debug>:/MTd> - $<$<NOT:$<CONFIG:Debug>>:/MT>) + if(msvc_runtime_policy) + _cc_static_runtime_via_policy("${target}") + else() + _cc_replace_flags("/MDd" "/MTd") + _cc_replace_flags("/MD" "/MT") + endif() endfunction() function(_cc_static_runtime_gcc target) @@ -148,10 +209,51 @@ function(_cc_static_runtime_gcc target) # target_link_libraries: #target_link_libraries("${target}" PRIVATE -static) - set_property(TARGET "${target}" APPEND_STRING PROPERTY LINK_FLAGS " -static") + set(flags -static-libstdc++ -static-libgcc) + if(CYGWIN) + set(flags -static-libgcc) + endif() + + if(CMAKE_VERSION VERSION_LESS "3.13") + _cc_join(flags_str " " ${flags}) + set_property(TARGET "${target}" APPEND_STRING PROPERTY LINK_FLAGS " ${flags_str}") + else() + target_link_options("${target}" PRIVATE ${flags}) + endif() +endfunction() + +function(_cc_static_runtime_clang target) + if(NOT WIN32) + # On Linux, clang/clang++ is used, which is treated as GCC. + # This is consistent with CMake (see Modules/Platform/Linux-Clang-CXX.cmake). + _cc_static_runtime_gcc("${target}") + return() + endif() + + # On Windows, clang/clang++ can be used since 3.15; otherwise, clang-cl is + # is used, which is treated as MSVC. + # This is consistent with CMake (see Modules/Platform/Windows-Clang.cmake). + if(CMAKE_VERSION VERSION_LESS "3.15") + _cc_static_runtime_msvc("${target}") + return() + endif() + + # If the policy is enabled, we don't need to patch the flags manually. + if(msvc_runtime_policy) + _cc_static_runtime_via_policy("${target}") + return() + endif() + + if("${CMAKE_CXX_COMPILER_FRONTEND_VARIANT}" STREQUAL "MSVC" OR "${CMAKE_C_COMPILER_FRONTEND_VARIANT}" STREQUAL "MSVC") + # It's 3.15 or higher, but we're in luck: clang-cl is used, which can + # be treated as MSVC. + _cc_static_runtime_msvc("${target}") + return() + endif() - # Or (haven't tested this), if CMake 3.13+ is used: - #target_link_options("${target}" PRIVATE -static) + # Well, that sucks, but works for versions 3.15--3.18 at least. + _cc_replace_flags("-D_DLL" "") + _cc_replace_flags("--dependent-lib=msvcrt" "--dependent-lib=libcmt") endfunction() function(_cc_static_runtime target) @@ -163,11 +265,14 @@ function(_cc_static_runtime target) _cc_static_runtime_msvc("${target}") elseif(is_gcc) _cc_static_runtime_gcc("${target}") + elseif(is_clang) + _cc_static_runtime_clang("${target}") endif() endif() endfunction() -# Symbol stripping: +# Symbol stripping +# ---------------- function(_cc_strip_symbols_gcc target) # This causes issues with mixing keyword- and plain- versions of @@ -183,13 +288,14 @@ function(_cc_strip_symbols target) get_target_property(aliased "${target}" ALIASED_TARGET) if(NOT target_type STREQUAL "INTERFACE_LIBRARY" AND NOT aliased) message(STATUS "common.cmake: ${target}: Stripping symbols for release configurations") - if(is_gcc) + if(is_gcc OR is_clang) _cc_strip_symbols_gcc("${target}") endif() endif() endfunction() -# Main macros: +# Main macros +# ----------- function(_cc_apply_settings target) if(TARGET "${target}") diff --git a/examples/boost/foo.cpp b/examples/boost/foo.cpp index 3bd0326..6219e68 100644 --- a/examples/boost/foo.cpp +++ b/examples/boost/foo.cpp @@ -3,9 +3,11 @@ #include <iostream> int main(int argc, char* argv[]) { - std::cout << "Hello from " - << boost::filesystem::absolute(boost::filesystem::path{argv[0]}) - .string() - << "!\n"; + namespace fs = boost::filesystem; + std::cout << argv[0] << '\n'; + for (int i = 1; i < argc; ++i) { + std::cout << fs::absolute(boost::filesystem::path{argv[i]}).string() + << '\n'; + } return 0; } diff --git a/examples/dynamic/foo.cpp b/examples/dynamic/foo.cpp index b7d9986..7e928e1 100644 --- a/examples/dynamic/foo.cpp +++ b/examples/dynamic/foo.cpp @@ -1,6 +1,9 @@ #include <baz.hpp> +#include <iostream> + int main() { + std::cout << "foo\n"; baz(); return 0; } diff --git a/examples/static/foo.cpp b/examples/static/foo.cpp index c6355a2..34cd82e 100644 --- a/examples/static/foo.cpp +++ b/examples/static/foo.cpp @@ -1,6 +1,9 @@ #include <bar.hpp> +#include <iostream> + int main() { + std::cout << "foo\n"; bar(); return 0; } diff --git a/project/boost/build.py b/project/boost/build.py index a9fe8da..3a073f3 100644 --- a/project/boost/build.py +++ b/project/boost/build.py @@ -5,8 +5,8 @@ R'''Build Boost. -This script builds the Boost libraries. It's main utility is setting the -correct --stagedir parameter value to avoid name clashes. +This script builds the Boost libraries. It main utility is setting the correct +--stagedir parameter value to avoid name clashes. Usage example: @@ -46,6 +46,7 @@ import sys import tempfile from project.boost.directory import BoostDir +from project.toolchain import ToolchainType from project.boost.toolchain import Toolchain from project.configuration import Configuration from project.linkage import Linkage @@ -60,14 +61,14 @@ DEFAULT_CONFIGURATIONS = (Configuration.DEBUG, Configuration.RELEASE,) # binaries from a CI, etc. and run them everywhere): DEFAULT_LINK = (Linkage.STATIC,) DEFAULT_RUNTIME_LINK = Linkage.STATIC -# Shut compilers up: -COMMON_B2_ARGS = ['-d0'] +B2_QUIET = ['-d0'] +B2_VERBOSE = ['-d2', '--debug-configuration'] class BuildParameters: def __init__(self, boost_dir, build_dir=None, platforms=None, configurations=None, link=None, runtime_link=None, - mingw=False, b2_args=None): + toolset=None, verbose=False, b2_args=None): boost_dir = normalize_path(boost_dir) if build_dir is not None: @@ -76,10 +77,12 @@ class BuildParameters: configurations = configurations or DEFAULT_CONFIGURATIONS link = link or DEFAULT_LINK runtime_link = runtime_link or DEFAULT_RUNTIME_LINK + toolset = toolset or ToolchainType.AUTO + verbosity = B2_VERBOSE if verbose else B2_QUIET if b2_args: - b2_args = COMMON_B2_ARGS + b2_args + b2_args = verbosity + b2_args else: - b2_args = COMMON_B2_ARGS + b2_args = verbosity self.boost_dir = boost_dir self.build_dir = build_dir @@ -88,17 +91,20 @@ class BuildParameters: self.configurations = configurations self.link = link self.runtime_link = runtime_link - self.mingw = mingw + self.toolset = toolset self.b2_args = b2_args @staticmethod def from_args(args): return BuildParameters(**vars(args)) + def get_bootstrap_args(self): + return self.toolset.get_bootstrap_args() + def enum_b2_args(self): with self._create_build_dir() as build_dir: for platform in self.platforms: - with Toolchain.detect(platform, mingw=self.mingw) as toolchain: + with Toolchain.detect(self.toolset, platform) as toolchain: for configuration in self.configurations: for link, runtime_link in self._linkage_options(): yield self._build_params(build_dir, toolchain, @@ -138,9 +144,9 @@ class BuildParameters: params.append(self._stagedir(toolchain, configuration)) params.append('--layout=system') params += toolchain.get_b2_args() + params.append(self._variant(configuration)) params.append(self._link(link)) params.append(self._runtime_link(runtime_link)) - params.append(self._variant(configuration)) params += self.b2_args return params @@ -204,8 +210,10 @@ def _parse_args(argv=None): type=Linkage.parse, default=DEFAULT_RUNTIME_LINK, help=f'how the libraries link to the runtime ({linkage_options})') - parser.add_argument('--mingw', action='store_true', - help='build using MinGW-w64') + toolset_options = '/'.join(map(str, ToolchainType.all())) + parser.add_argument('--toolset', metavar='TOOLSET', + type=ToolchainType.parse, default=ToolchainType.AUTO, + help=f'toolset to use ({toolset_options})') parser.add_argument('--build', metavar='DIR', dest='build_dir', type=normalize_path, @@ -214,6 +222,8 @@ def _parse_args(argv=None): type=normalize_path, help='root Boost directory') + parser.add_argument('-v', '--verbose', action='store_true', + help='verbose b2 invocation (quiet by default)') parser.add_argument('b2_args', metavar='B2_ARG', nargs='*', default=[], help='additional b2 arguments, to be passed verbatim') diff --git a/project/boost/directory.py b/project/boost/directory.py index 17448b6..9b35194 100644 --- a/project/boost/directory.py +++ b/project/boost/directory.py @@ -6,6 +6,7 @@ import logging import os.path +from project.boost.toolchain import BootstrapToolchain from project.utils import cd, run from project.os import on_windows @@ -21,18 +22,19 @@ class BoostDir: def build(self, params): with self._go(): - self._bootstrap_if_required() + self._bootstrap_if_required(params) self._b2(params) - def _bootstrap_if_required(self): + def _bootstrap_if_required(self, params): if os.path.isfile(self._b2_path()): logging.info('Not going to bootstrap, b2 is already there') return - self.bootstrap() + self.bootstrap(params) - def bootstrap(self): + def bootstrap(self, params): with self._go(): - run(self._bootstrap_path()) + toolchain = BootstrapToolchain.detect(params.toolset) + run([self._bootstrap_path()] + self._bootstrap_args(toolchain)) def _b2(self, params): for b2_params in params.enum_b2_args(): @@ -50,6 +52,12 @@ class BoostDir: return f'bootstrap{ext}' @staticmethod + def _bootstrap_args(toolchain): + if on_windows(): + return toolchain.get_bootstrap_bat_args() + return toolchain.get_bootstrap_sh_args() + + @staticmethod def _b2_path(): return os.path.join('.', BoostDir._b2_name()) diff --git a/project/boost/download.py b/project/boost/download.py index c3451a8..ca113a6 100644 --- a/project/boost/download.py +++ b/project/boost/download.py @@ -5,8 +5,8 @@ R'''Download & bootstrap Boost. -This script downloads and bootstraps a Boost distribution. It's main utility -is that it's supposed to be cross-platform. +This script downloads and unpacks a Boost distribution archive. Its main +utility is that it's supposed to be cross-platform. Usage examples: @@ -97,7 +97,6 @@ def download(params): with _download_if_necessary(params.version, params.storage) as path: archive = Archive(params.version, path) boost_dir = archive.unpack(params.unpack_dir) - boost_dir.bootstrap() params.rename_if_necessary(boost_dir) diff --git a/project/boost/toolchain.py b/project/boost/toolchain.py index a77c073..cce06a4 100644 --- a/project/boost/toolchain.py +++ b/project/boost/toolchain.py @@ -3,78 +3,455 @@ # For details, see https://github.com/egor-tensin/cmake-common. # Distributed under the MIT License. -R'''Compiler detection. - -It is assumed that Boost.Build is good enough to detect both GCC on Linux and -MSVC on Windows. From that point on, it's just a matter of setting the correct -address-model= value. - -But I also frequently use MinGW-w64, and the most convinient way to use it that -I know is making a "user config" and passing it to b2 using the --user-config -parameter. -''' +# Hate speech +# ----------- +# +# Is there a person who doesn't hate Boost.Build? I'm not sure, I'm definitely +# _not_ one of these people. Maybe it's the lack of adoption (meaning that +# learning it is useless outside of Boost), maybe it's the incomprehensible +# syntax. Maybe it's the absolutely insane compiler-specific configuration +# files (tools/build/src/tools/*.jam), which are impossible to figure out. +# Maybe it's the fact that the implementation switched from C to C++ while some +# half-baked Python implementation has been there since at least 2015 (see the +# marvelous memo "Status: mostly ported." at the top of tools/build/src/build_system.py). +# +# What I hate the most though is how its various subtle, implicit and invisible +# decision-making heuristics changed thoughout the release history of Boost. +# You have a config and a compiler that will happily build version 1.65.0? +# Great! Want to use the same config and the same compiler to build version +# 1.72.0? Well, too fucking bad, it doesn't work anymore. This I really do +# hate the most. +# +# Three kinds of toolsets +# ----------------------- +# +# b2 accepts the toolset= parameter. What about building b2 itself though? +# Well, this is what the bootstrap.{sh,bat} scripts do. They also accept a +# toolset argument, but it is _completely_ different to that of b2. That's +# sort of OK, since e.g. cross-compiling b2 is something we rarely want to do +# (and hence there must typically be a native toolset available). +# +# bootstrap.sh and bootstrap.bat are completely different (of course!), and +# accept different arguments for their toolset parameters. +# +# Config file insanity +# -------------------- +# +# Say, we're building Boost on Windows using the GCC from a MinGW-w64 +# distribution. We can pass toolset=gcc and all the required flags on the +# command line no problem. What if we want to make a user configuration file +# so that 1) the command line is less polluted, and 2) it can possibly be +# shared? Well, if we put +# +# using gcc : : : <name>value... ; +# +# there, Boost 1.65.0 will happily build everything, while Boost 1.72.0 will +# complain about "duplicate initialization of gcc". This is because when we +# ran `bootstrap.bat gcc` earlier, it wrote `using gcc ;` in project-config.jam. +# And while Boost 1.65.0 detects that toolset=gcc means we're going to use the +# MinGW GCC, and magically turns toolset=gcc to toolset=gcc-mingw, Boost 1.72.0 +# does no such thing, and chokes on the "duplicate" GCC declaration. +# +# We also cannot put +# +# using gcc : custom : : <options> ; +# +# without the executable path, since Boost insists that `g++ -dumpversion` must +# equal to "custom" (which makes total sense, lol). So we have to force it, +# and do provide the path. +# +# Windows & Clang +# --------------- +# +# Building Boost using Clang on Windows is a sad story. As of 2020, there're +# three main ways to install the native Clang toolchain on Windows: +# +# * download the installer from llvm.org (`choco install llvm` does this) +# a.k.a. the upstream, +# * install it as part of a MSYS2 installation (`pacman -S mingw-w64-x86_64-clang`), +# * install as part of a Visual Studio installation. +# +# Using the latter method, you can switch a project to use the LLVM toolset +# using Visual Studio, but that's stupid. The former two, on the other hand, +# give us the the required clang/clang++/clang-cl executables, so everything +# seems to be fine. +# +# Except it's not fine. Let's start with the fact that prior to 1.66.0, +# toolset=clang is completely broken on Windows. It's just an alias for +# clang-linux, and it's hardcoded to require the ar & ranlib executables to +# create static libraries. Which is fine on Linux, since, and I'm quoting the +# source, "ar is always available". But it's not fine on Windows, since +# ar/ranlib are not, in fact, available there by default. Sure, you can +# install some kind of MinGW toolchain, and it might even work, but what the +# hell, honestly? +# +# Luckily, both the upstream distribution and the MSYS2 mingw-w64-x86_64-llvm +# package come with the llvm-ar and llvm-ranlib utilities. So we can put +# something like this in the config: +# +# using clang : custom : clang++.exe : <archiver>llvm-ar <ranlib>llvm-ranlib.exe ; +# +# and later call +# +# b2 toolset=clang-custom --user-config=path/to/config.jam ... +# +# But, as I mentioned, prior to 1.66.0, toolset=clang is _hardcoded_ to use ar +# & ranlib, these exact utility names. So either get them as part of some +# MinGW distribution or build Boost using another toolset. +# +# Now, it's all fine, but building stuff on Windows adds another thing into the +# equation: debug runtimes. When you build Boost using MSVC, for example, it +# picks one of the appropriate /MT[d] or /MD[d] flags to build the Boost +# libraries with. Emulating these flags with toolset=clang is complicated and +# inconvenient. Luckily, there's the clang-cl.exe executable, which aims to +# provide command line interface compatible with that of cl.exe. +# +# Boost.Build even supports toolset=clang-win, which should use clang-cl.exe. +# But alas, it's completely broken prior to 1.69.0. It just doesn't work at +# all. So, if you want to build w/ clang-cl.exe, either use Boost 1.69.0 or +# later, or build using another toolset. +# +# Cygwin & Clang +# -------------- +# +# Now, a few words about Clang on Cygwin. When building 1.65.0, I encountered +# the following error: +# +# /usr/include/w32api/synchapi.h:127:26: error: conflicting types for 'Sleep' +# WINBASEAPI VOID WINAPI Sleep (DWORD dwMilliseconds); +# ^ +# ./boost/smart_ptr/detail/yield_k.hpp:64:29: note: previous declaration is here +# extern "C" void __stdcall Sleep( unsigned long ms ); +# ^ +# +# GCC doesn't emit an error here because /usr/include is in a pre-configured +# "system" include directories list, and the declaration there take precedence, +# I guess? The root of the problem BTW is that sizeof(unsigned long) is +# +# * 4 for MSVC and MinGW-born GCCs, +# * 8 for Clang (and, strangely, Cygwin GCC; why don't we get runtime +# errors?). +# +# The fix is to add `define=BOOST_USE_WINDOWS_H`. I don't even know what's the +# point of not having it as a default. import abc from contextlib import contextmanager import logging +import os.path +import shutil import project.mingw +import project.os +from project.toolchain import ToolchainType from project.utils import temp_file +class BootstrapToolchain(abc.ABC): + @abc.abstractmethod + def get_bootstrap_bat_args(self): + pass + + @abc.abstractmethod + def get_bootstrap_sh_args(self): + pass + + @staticmethod + def detect(hint): + if hint is ToolchainType.AUTO: + return BootstrapAuto() + if hint is ToolchainType.MSVC: + return BootstrapMSVC() + if hint is ToolchainType.GCC: + return BootstrapGCC() + if hint is ToolchainType.MINGW: + return BootstrapMinGW() + if hint is ToolchainType.CLANG: + return BootstrapClang() + if hint is ToolchainType.CLANG_CL: + return BootstrapClangCL() + raise NotImplementedError(f'unrecognized toolset: {hint}') + + +class BootstrapAuto(BootstrapToolchain): + # Let Boost.Build do the detection. Most commonly it means GCC on + # Linux-likes and MSVC on Windows. + + def get_bootstrap_bat_args(self): + return [] + + def get_bootstrap_sh_args(self): + return [] + + +class BootstrapMSVC(BootstrapAuto): + # bootstrap.bat picks up MSVC by default. + pass + + +class BootstrapGCC(BootstrapToolchain): + def get_bootstrap_bat_args(self): + return ['gcc'] + + def get_bootstrap_sh_args(self): + return ['--with-toolset=gcc'] + + +def _gcc_or_auto(): + if shutil.which('gcc') is not None: + return ['gcc'] + return [] + + +class BootstrapMinGW(BootstrapToolchain): + def get_bootstrap_bat_args(self): + # On Windows, prefer GCC if it's available. + return _gcc_or_auto() + + def get_bootstrap_sh_args(self): + return [] + + +class BootstrapClang(BootstrapToolchain): + def get_bootstrap_bat_args(self): + # As of 1.74.0, bootstrap.bat isn't really aware of Clang, so try GCC, + # then auto-detect. + return _gcc_or_auto() + + def get_bootstrap_sh_args(self): + # bootstrap.sh, on the other hand, is very much aware of Clang, and + # it can build b2 using this compiler. + return ['--with-toolset=clang'] + + +class BootstrapClangCL(BootstrapClang): + # There's no point in building b2 using clang-cl; clang though, presumably + # installed alongside clang-cl, should still be used if possible. + pass + + class Toolchain(abc.ABC): def __init__(self, platform): self.platform = platform - @abc.abstractmethod def get_b2_args(self): - pass + return [ + # Always pass the address-model explicitly. + f'address-model={self.platform.get_address_model()}' + ] @staticmethod @contextmanager - def detect(platform, mingw=False): - if mingw: + def detect(hint, platform): + if hint is ToolchainType.AUTO: + yield Auto(platform) + elif hint is ToolchainType.MSVC: + yield MSVC(platform) + elif hint is ToolchainType.GCC: + with GCC.setup(platform) as toolchain: + yield toolchain + elif hint is ToolchainType.MINGW: with MinGW.setup(platform) as toolchain: yield toolchain + elif hint is ToolchainType.CLANG: + with Clang.setup(platform) as toolchain: + yield toolchain + elif hint is ToolchainType.CLANG_CL: + yield ClangCL(platform) else: - yield Native(platform) + raise NotImplementedError(f'unrecognized toolset: {hint}') - @staticmethod - def _format_user_config(tag, compiler, **kwargs): - features = (f'<{k}>{v}' for k, v in kwargs.items()) - features = ' '.join(features) - return f'using gcc : {tag} : {compiler} : {features} ;' +class Auto(Toolchain): + # Let Boost.Build do the detection. Most commonly it means GCC on + # Linux-likes and MSVC on Windows. + pass -class Native(Toolchain): + +class MSVC(Auto): def get_b2_args(self): - return [f'address-model={self.platform.get_address_model()}'] + return super().get_b2_args() + [ + 'toolset=msvc', + ] + +def _full_exe_name(exe): + if project.os.on_linux(): + # There's no PATHEXT on Linux. + return exe + # b2 on Windows/Cygwin doesn't like it when the executable name doesn't + # include the extension. + dir_path = os.path.dirname(exe) or None + path = shutil.which(exe, path=dir_path) + if not path: + raise RuntimeError(f"executable '{exe}' could not be found") + if project.os.on_cygwin(): + # On Cygwin, shutil.which('gcc') == '/usr/bin/gcc' and shutil.which('gcc.exe') + # == '/usr/bin/gcc.exe'; we want the latter version. shutil.which('clang++') + # == '/usr/bin/clang++' is fine though, since it _is_ the complete path + # (clang++ is a symlink). + if os.path.exists(path) and os.path.exists(path + '.exe'): + path += '.exe' + if dir_path: + # If it was found in a specific directory, include the directory in the + # result. shutil.which returns the executable name prefixed with the + # path argument. + return path + # If it was found in PATH, just return the basename (which includes the + # extension). + return os.path.basename(path) -class MinGW(Toolchain): - TAG = 'custom' - def __init__(self, platform, config_path): +class BoostBuildToolset: + CUSTOM = 'custom' + + def __init__(self, compiler, path, options): + if not compiler: + raise RuntimeError('compiler type is required (like gcc, clang, etc.)') + self.compiler = compiler + self.version = BoostBuildToolset.CUSTOM + path = path or '' + path = path and _full_exe_name(path) + self.path = path + options = options or [] + self.options = options + + @property + def toolset_id(self): + if self.version: + return f'{self.compiler}-{self.version}' + return self.compiler + + @property + def b2_arg(self): + return f'toolset={self.toolset_id}' + + def _format_using_options(self): + return ''.join(f'\n <{name}>{val}' for name, val in self.options) + + def format_using(self): + version = self.version and f'{self.version} ' + path = self.path and f'{self.path} ' + return f'''using {self.compiler} : {version}: {path}:{self._format_using_options()} +;''' + + +class ConfigFile(Toolchain): + def __init__(self, platform, config_path, toolset): super().__init__(platform) self.config_path = config_path - - def get_b2_args(self): - return [f'--user-config={self.config_path}', f'toolset=gcc-{MinGW.TAG}'] + self.toolset = toolset @staticmethod - def _format_mingw_user_config(platform): - compiler = project.mingw.get_gxx(platform) - features = { - 'target-os': 'windows', - 'address-model': platform.get_address_model(), - } - return Toolchain._format_user_config(MinGW.TAG, compiler, **features) + @abc.abstractmethod + def get_toolset(platform): + pass @staticmethod + @abc.abstractmethod + def format_config(toolset): + pass + + @classmethod @contextmanager - def setup(platform): - config = MinGW._format_mingw_user_config(platform) + def setup(cls, platform): + toolset = cls.get_toolset(platform) + config = cls.format_config(toolset) logging.info('Using user config:\n%s', config) - tmp = temp_file(config, mode='w', prefix='mingw_w64_', suffix='.jam') + tmp = temp_file(config, mode='w', prefix='user_config_', suffix='.jam') with tmp as path: - yield MinGW(platform, path) + yield cls(platform, path, toolset) + + def get_b2_args(self): + # All the required options and the toolset definition should be in the + # user configuration file. + return super().get_b2_args() + [ + f'--user-config={self.config_path}', + self.toolset.b2_arg, + ] + + +class GCC(ConfigFile): + # Force GCC. We don't care whether it's a native Linux GCC or a + # MinGW-flavoured GCC on Windows. + COMPILER = 'gcc' + + @staticmethod + def get_options(): + return [ + # TODO: this is a petty attempt to get rid of build warnings in + # older Boost versions. Revise and expand this list or remove it? + # warning: 'template<class> class std::auto_ptr' is deprecated + ('cxxflags', '-Wno-deprecated-declarations'), + # warning: unnecessary parentheses in declaration of 'assert_arg' + ('cxxflags', '-Wno-parentheses'), + ] + + @staticmethod + def get_toolset(platform): + return BoostBuildToolset(GCC.COMPILER, 'g++', GCC.get_options()) + + @staticmethod + def format_config(toolset): + return toolset.format_using() + + +class MinGW(GCC): + # It's important that Boost.Build is actually smart enough to detect the + # GCC prefix (like "x86_64-w64-mingw32" and prepend it to other tools like + # "ar"). + + @staticmethod + def get_toolset(platform): + path = project.mingw.get_gxx(platform) + return BoostBuildToolset(MinGW.COMPILER, path, MinGW.get_options()) + + +class Clang(ConfigFile): + COMPILER = 'clang' + + @staticmethod + def get_toolset(platform): + options = [ + ('cxxflags', '-DBOOST_USE_WINDOWS_H'), + # TODO: this is a petty attempt to get rid of build warnings in + # older Boost versions. Revise and expand this list or remove it? + # warning: unused typedef 'boost_concept_check464' [-Wunused-local-typedef] + ('cxxflags', '-Wno-unused-local-typedef'), + # error: constant expression evaluates to -105 which cannot be narrowed to type 'boost::re_detail::cpp_regex_traits_implementation<char>::char_class_type' (aka 'unsigned int') + ('cxxflags', '-Wno-c++11-narrowing'), + ] + GCC.get_options() + if project.os.on_windows(): + # Prefer LLVM binutils: + if shutil.which('llvm-ar') is not None: + options.append(('archiver', 'llvm-ar')) + if shutil.which('llvm-ranlib') is not None: + options.append(('ranlib', 'llvm-ranlib')) + return BoostBuildToolset(Clang.COMPILER, 'clang++', options) + + @staticmethod + def format_config(toolset): + # To make clang.exe/clang++.exe work on Windows, some tweaks are + # required. I borrowed them from CMake's Windows-Clang.cmake [1]. + # Adding them globally to Boost.Build options is described in [2]. + # + # [1]: https://github.com/Kitware/CMake/blob/v3.18.4/Modules/Platform/Windows-Clang.cmake + # [2]: https://stackoverflow.com/questions/2715106/how-to-create-a-new-variant-in-bjam + return f'''project : requirements + <target-os>windows:<define>_MT + <target-os>windows,<variant>debug:<define>_DEBUG + <target-os>windows,<runtime-link>static,<variant>debug:<cxxflags>"-Xclang -flto-visibility-public-std -Xclang --dependent-lib=libcmtd" + <target-os>windows,<runtime-link>static,<variant>release:<cxxflags>"-Xclang -flto-visibility-public-std -Xclang --dependent-lib=libcmt" + <target-os>windows,<runtime-link>shared,<variant>debug:<cxxflags>"-D_DLL -Xclang --dependent-lib=msvcrtd" + <target-os>windows,<runtime-link>shared,<variant>release:<cxxflags>"-D_DLL -Xclang --dependent-lib=msvcrt" +; +{toolset.format_using()} +''' + + +class ClangCL(Toolchain): + def get_b2_args(self): + return super().get_b2_args() + [ + f'toolset=clang-win', + 'define=BOOST_USE_WINDOWS_H', + ] diff --git a/project/ci/boost.py b/project/ci/boost.py index c17c9db..3da5c9b 100644 --- a/project/ci/boost.py +++ b/project/ci/boost.py @@ -7,8 +7,9 @@ import argparse import logging import sys -from project.boost.download import DownloadParameters, download from project.boost.build import BuildParameters, build +from project.boost.download import DownloadParameters, download +from project.boost.toolchain import ToolchainType from project.linkage import Linkage @@ -27,8 +28,10 @@ def _parse_args(dirs, argv=None): parser.add_argument('--runtime-link', metavar='LINKAGE', type=Linkage.parse, help='how the libraries link to the runtime') - parser.add_argument('--mingw', action='store_true', - help='build using MinGW-w64') + parser.add_argument('--toolset', metavar='TOOLSET', + type=ToolchainType.parse, + help='toolset to use') + parser.add_argument('b2_args', metavar='B2_ARG', nargs='*', default=[], help='additional b2 arguments, to be passed verbatim') @@ -50,6 +53,6 @@ def build_ci(dirs, argv=None): configurations=(dirs.get_configuration(),), link=args.link, runtime_link=args.runtime_link, - mingw=args.mingw, + toolset=args.toolset, b2_args=args.b2_args) build(params) diff --git a/project/ci/cmake.py b/project/ci/cmake.py index 4a58749..df2b55a 100644 --- a/project/ci/cmake.py +++ b/project/ci/cmake.py @@ -8,6 +8,7 @@ import logging import sys from project.cmake.build import BuildParameters, build +from project.toolchain import ToolchainType def _parse_args(dirs, argv=None): @@ -23,8 +24,9 @@ def _parse_args(dirs, argv=None): help='install directory') parser.add_argument('--boost', metavar='DIR', dest='boost_dir', help='set Boost directory path') - parser.add_argument('--mingw', action='store_true', - help='build using MinGW-w64') + parser.add_argument('--toolset', metavar='TOOLSET', + type=ToolchainType.parse, + help=f'toolset to use') parser.add_argument('cmake_args', nargs='*', metavar='CMAKE_ARG', default=[], help='additional CMake arguments, to be passed verbatim') return parser.parse_args(argv) @@ -39,6 +41,6 @@ def build_ci(dirs, argv=None): platform=dirs.get_platform(), configuration=dirs.get_configuration(), boost_dir=args.boost_dir or dirs.get_boost_dir(), - mingw=args.mingw, + toolset=args.toolset, cmake_args=dirs.get_cmake_args() + args.cmake_args) build(params) diff --git a/project/cmake/build.py b/project/cmake/build.py index e683eff..6bc7772 100644 --- a/project/cmake/build.py +++ b/project/cmake/build.py @@ -30,10 +30,13 @@ import tempfile from project.cmake.toolchain import Toolchain from project.configuration import Configuration from project.platform import Platform +from project.toolchain import ToolchainType from project.utils import normalize_path, run, setup_logging +DEFAULT_PLATFORM = Platform.native() DEFAULT_CONFIGURATION = Configuration.DEBUG +DEFAULT_TOOLSET = ToolchainType.AUTO def run_cmake(cmake_args): @@ -41,15 +44,18 @@ def run_cmake(cmake_args): class GenerationPhase: - def __init__(self, src_dir, build_dir, install_dir=None, - platform=None, configuration=DEFAULT_CONFIGURATION, - boost_dir=None, mingw=False, cmake_args=None): + def __init__(self, src_dir, build_dir, install_dir=None, platform=None, + configuration=None, boost_dir=None, toolset=None, + cmake_args=None): src_dir = normalize_path(src_dir) build_dir = normalize_path(build_dir) if install_dir is not None: install_dir = normalize_path(install_dir) + platform = platform or DEFAULT_PLATFORM + configuration = configuration or DEFAULT_CONFIGURATION if boost_dir is not None: boost_dir = normalize_path(boost_dir) + toolset = toolset or DEFAULT_TOOLSET cmake_args = cmake_args or [] self.src_dir = src_dir @@ -58,7 +64,7 @@ class GenerationPhase: self.platform = platform self.configuration = configuration self.boost_dir = boost_dir - self.mingw = mingw + self.toolset = toolset self.cmake_args = cmake_args def _cmake_args(self, toolchain): @@ -87,44 +93,47 @@ class GenerationPhase: platform = Platform.native() return os.path.join(boost_dir, 'stage', str(platform), str(configuration), 'lib') - def run(self): - with Toolchain.detect(self.platform, self.build_dir, mingw=self.mingw) as toolchain: - run_cmake(self._cmake_args(toolchain)) + def run(self, toolchain): + run_cmake(self._cmake_args(toolchain)) class BuildPhase: - def __init__(self, build_dir, install_dir=None, - configuration=DEFAULT_CONFIGURATION): + def __init__(self, build_dir, install_dir=None, configuration=None): build_dir = normalize_path(build_dir) + configuration = configuration or DEFAULT_CONFIGURATION self.build_dir = build_dir self.install_dir = install_dir self.configuration = configuration - def _cmake_args(self): + def _cmake_args(self, toolchain): result = ['--build', self.build_dir] result += ['--config', str(self.configuration)] if self.install_dir is not None: result += ['--target', 'install'] + result += ['--'] + toolchain.get_build_args() return result - def run(self): - run_cmake(self._cmake_args()) + def run(self, toolchain): + run_cmake(self._cmake_args(toolchain)) class BuildParameters: def __init__(self, src_dir, build_dir=None, install_dir=None, - platform=None, configuration=DEFAULT_CONFIGURATION, - boost_dir=None, mingw=False, cmake_args=None): + platform=None, configuration=None, boost_dir=None, + toolset=None, cmake_args=None): src_dir = normalize_path(src_dir) if build_dir is not None: build_dir = normalize_path(build_dir) if install_dir is not None: install_dir = normalize_path(install_dir) + platform = platform or DEFAULT_PLATFORM + configuration = configuration or DEFAULT_CONFIGURATION if boost_dir is not None: boost_dir = normalize_path(boost_dir) + toolset = toolset or DEFAULT_TOOLSET cmake_args = cmake_args or [] self.src_dir = src_dir @@ -133,7 +142,7 @@ class BuildParameters: self.platform = platform self.configuration = configuration self.boost_dir = boost_dir - self.mingw = mingw + self.toolset = toolset self.cmake_args = cmake_args @staticmethod @@ -158,17 +167,19 @@ class BuildParameters: def build(params): with params.create_build_dir() as build_dir: + toolchain = Toolchain.detect(params.toolset, params.platform, build_dir) + gen_phase = GenerationPhase(params.src_dir, build_dir, install_dir=params.install_dir, platform=params.platform, configuration=params.configuration, boost_dir=params.boost_dir, - mingw=params.mingw, + toolset=params.toolset, cmake_args=params.cmake_args) - gen_phase.run() + gen_phase.run(toolchain) build_phase = BuildPhase(build_dir, install_dir=params.install_dir, configuration=params.configuration) - build_phase.run() + build_phase.run(toolchain) def _parse_args(argv=None): @@ -201,8 +212,10 @@ def _parse_args(argv=None): type=normalize_path, help='set Boost directory path') - parser.add_argument('--mingw', action='store_true', - help='build using MinGW-w64') + toolset_options = '/'.join(map(str, ToolchainType.all())) + parser.add_argument('--toolset', metavar='TOOLSET', + type=ToolchainType.parse, default=ToolchainType.AUTO, + help=f'toolset to use ({toolset_options})') parser.add_argument('src_dir', metavar='DIR', type=normalize_path, diff --git a/project/cmake/toolchain.py b/project/cmake/toolchain.py index 073cd1b..7c96628 100644 --- a/project/cmake/toolchain.py +++ b/project/cmake/toolchain.py @@ -3,13 +3,127 @@ # For details, see https://github.com/egor-tensin/cmake-common. # Distributed under the MIT License. +# Default generator +# ----------------- +# +# As of CMake 3.18, the default generator (unless set explicitly) is: +# * the newest Visual Studio or "NMake Makefiles" on Windows, +# * "Unix Makefiles" otherwise. +# This is regardless of whether any executables like gcc, cl or make are +# available [1]. +# +# Makefile generators +# ------------------- +# +# CMake has a number of "... Makefiles" generators. "Unix Makefiles" uses +# gmake/make/smake, whichever is found first, and cc/c++ for compiler +# detection [2]. "MinGW Makefiles" looks for mingw32-make.exe in a number of +# well-known locations, uses gcc/g++ directly, and is aware of windres [3]. In +# addition, "Unix Makefiles" uses /bin/sh as the SHELL value in the Makefile, +# while the MinGW version uses cmd.exe. I don't think it matters on Windows +# though, since the non-existent /bin/sh is ignored anyway [4]. "NMake +# Makefiles" is similar, except it defaults to using cl [5]. +# +# It's important to _not_ use the -A parameter with any of the Makefile +# generators - it's an error. This goes for "NMake Makefiles" also. "NMake +# Makefiles" doesn't attempt to search for installed Visual Studio compilers, +# you need to use it from one of the Visual Studio-provided shells. +# +# Visual Studio generators +# ------------------------ +# +# These are special. They ignore the CMAKE_<LANG>_COMPILER parameters and use +# cl by default [9]. They support specifying the toolset to use via the -T +# parameter (the "Platform Toolset" value in the project's properties) since +# 3.18 [10]. The toolset list varies between Visual Studio versions, and I'm +# too lazy to learn exactly which version supports which toolsets. +# +# cmake --build uses msbuild with Visual Studio generators. You can pass the +# path to a different cl.exe by doing something like +# +# msbuild ... /p:CLToolExe=another-cl.exe /p:CLToolPath=C:\parent\dir +# +# It's important that the generators for Visual Studio 2017 or older use Win32 +# Win32 as the default platform [12]. Because of that, we need to pass the -A +# parameter. +# +# mingw32-make vs make +# -------------------- +# +# No idea what the actual differences are. The explanation in the FAQ [6] +# about how GNU make "is lacking in some functionality and has modified +# functionality due to the lack of POSIX on Win32" isn't terribly helpful. +# +# It's important that you can install either on Windows (`choco install make` +# for GNU make and `choco install mingw` to install a MinGW-w64 distribution +# with mingw32-make.exe included). Personally, I don't see any difference +# between using either make.exe or mingw32-make.exe w/ CMake on Windows. But, +# since MinGW-w64 distributions do include mingw32-make.exe and not make.exe, +# we'll try to detect that. +# +# Cross-compilation +# ----------------- +# +# If you want to e.g. build x86 binary on x64 and vice versa, the easiest way +# seems to be to make a CMake "toolchain file", which initializes the proper +# compiler flags (like -m64/-m32, etc.). Such file could look like this: +# +# set(CMAKE_C_COMPILER gcc) +# set(CMAKE_C_FLAGS -m32) +# set(CMAKE_CXX_COMPILER g++) +# set(CMAKE_CXX_FLAGS -m32) +# +# You can then pass the path to it using the CMAKE_TOOLCHAIN_FILE parameter. +# +# If you use the Visual Studio generators, just use the -A parameter, like "-A +# Win32". +# +# As a side note, if you want to cross-compile between x86 and x64 using GCC on +# Ubuntu, you need to install the gcc-multilib package. +# +# Windows & Clang +# --------------- +# +# Using Clang on Windows is no easy task, of course. Prior to 3.15, there was +# no support for building things using the clang++.exe executable, only +# clang-cl.exe was supported [7]. If you specified -DCMAKE_CXX_COMPILER=clang++, +# CMake would stil pass MSVC-style command line options to the compiler (like +# /MD, /nologo, etc.), which clang++ doesn't like [8]. +# +# So, in summary, you can only use clang++ since 3.15. clang-cl doesn't work +# with Visual Studio generators unless you specify the proper toolset using the +# -T parameter. You can set the ClToolExe property using msbuild, but while +# that might work in practice, clang-cl.exe needs to map some unsupported +# options for everything to work properly. For an example of how this is done, +# see the LLVM.Cpp.Common.* files at [11]. +# +# I recommend using Clang (either clang-cl or clang++ since 3.15) using the +# "NMake Makefiles" generator. +# +# References +# ---------- +# +# [1]: cmake::EvaluateDefaultGlobalGenerator +# https://github.com/Kitware/CMake/blob/v3.18.4/Source/cmake.cxx +# [2]: https://github.com/Kitware/CMake/blob/v3.18.4/Source/cmGlobalUnixMakefileGenerator3.cxx +# [3]: https://github.com/Kitware/CMake/blob/v3.18.4/Source/cmGlobalMinGWMakefileGenerator.cxx +# [4]: https://www.gnu.org/software/make/manual/html_node/Choosing-the-Shell.html +# [5]: https://github.com/Kitware/CMake/blob/v3.18.4/Source/cmGlobalNMakeMakefileGenerator.cxx +# [6]: http://mingw.org/wiki/FAQ +# [7]: https://cmake.org/cmake/help/v3.15/release/3.15.html#compilers +# [8]: https://github.com/Kitware/CMake/blob/v3.14.7/Modules/Platform/Windows-Clang.cmake +# [9]: https://gitlab.kitware.com/cmake/cmake/-/issues/19174 +# [10]: https://cmake.org/cmake/help/v3.8/release/3.8.html +# [11]: https://github.com/llvm/llvm-project/tree/e408935bb5339e20035d84307c666fbdd15e99e0/llvm/tools/msbuild +# [12]: https://cmake.org/cmake/help/v3.18/generator/Visual%20Studio%2015%202017.html + import abc -from contextlib import contextmanager import os.path +import shutil import project.mingw -from project.platform import Platform from project.os import on_windows +from project.toolchain import ToolchainType class Toolchain(abc.ABC): @@ -17,89 +131,164 @@ class Toolchain(abc.ABC): def get_cmake_args(self): pass - @staticmethod - @contextmanager - def detect(platform, build_dir, mingw=False): - if mingw: - with MinGW.setup(platform, build_dir) as toolchain: - yield toolchain - return - - if on_windows(): - # MSVC is assumed. - if platform is None: - yield Native() - return - yield MSVC(platform) - return + @abc.abstractmethod + def get_build_args(self): + pass - with GCC.setup(platform, build_dir) as toolchain: - yield toolchain - return + @staticmethod + def detect(hint, platform, build_dir): + if hint is ToolchainType.AUTO: + if on_windows(): + # We need to specify the -A parameter. This might break if + # none of the Visual Studio generators are available, but the + # NMake one is, although I don't know how this can be possible + # normally. + hint = ToolchainType.MSVC + else: + # Same thing for the -m32/-m64 flags. + hint = ToolchainType.GCC + if hint is ToolchainType.MSVC: + return MSVC(platform) + if hint is ToolchainType.GCC: + return GCC.setup(platform, build_dir) + if hint is ToolchainType.MINGW: + return MinGW.setup(platform, build_dir) + if hint is ToolchainType.CLANG: + return Clang.setup(platform, build_dir) + if hint is ToolchainType.CLANG_CL: + return ClangCL.setup(platform, build_dir) + raise NotImplementedError(f'unrecognized toolset: {hint}') -class Native(Toolchain): +class Auto(Toolchain): def get_cmake_args(self): return [] + def get_build_args(self): + return [] -class MSVC(Toolchain): + +class MSVC(Auto): def __init__(self, platform): self.platform = platform def get_cmake_args(self): + # This doesn't actually specify the generator of course, but I don't + # want to implement VS detection logic. return ['-A', self.platform.get_cmake_arch()] + def get_build_args(self): + return ['/m'] + -class File(Toolchain): +class Makefile(Toolchain): def __init__(self, path): self.path = path @staticmethod - def _get_path(build_dir): + def _get_config_path(build_dir): return os.path.join(build_dir, 'custom_toolchain.cmake') + def _get_makefile_generator(self): + if on_windows(): + if shutil.which('mingw32-make'): + return 'MinGW Makefiles' + return 'Unix Makefiles' + # On Linux/Cygwin, make all the way: + return 'Unix Makefiles' + + @classmethod + def write_config(cls, build_dir, contents): + path = Makefile._get_config_path(build_dir) + with open(path, mode='w') as file: + file.write(contents) + return cls(path) + def get_cmake_args(self): - return ['-D', f'CMAKE_TOOLCHAIN_FILE={self.path}'] + return [ + '-D', f'CMAKE_TOOLCHAIN_FILE={self.path}', + # The Visual Studio generator is the default on Windows, override + # it: + '-G', self._get_makefile_generator(), + ] + + def get_build_args(self): + return [] -class GCC(File): +class GCC(Makefile): @staticmethod def _format(platform): return f''' -set(CMAKE_C_COMPILER gcc) -set(CMAKE_C_FLAGS -m{platform.get_address_model()}) -set(CMAKE_CXX_COMIPLER g++) -set(CMAKE_CXX_FLAGS -m{platform.get_address_model()}) +set(CMAKE_C_COMPILER gcc) +set(CMAKE_C_FLAGS -m{platform.get_address_model()}) +set(CMAKE_CXX_COMPILER g++) +set(CMAKE_CXX_FLAGS -m{platform.get_address_model()}) ''' @staticmethod - @contextmanager def setup(platform, build_dir): - if platform is None: - yield Native() - return - path = File._get_path(build_dir) - with open(path, mode='w') as file: - file.write(GCC._format(platform)) - yield GCC(path) + return GCC.write_config(build_dir, GCC._format(platform)) -class MinGW(File): +class MinGW(Makefile): @staticmethod def _format(platform): return f''' set(CMAKE_C_COMPILER {project.mingw.get_gcc(platform)}) set(CMAKE_CXX_COMPILER {project.mingw.get_gxx(platform)}) -set(CMAKE_RC_COMILER {project.mingw.get_windres(platform)}) +set(CMAKE_AR {project.mingw.get_ar(platform)}) +set(CMAKE_RANLIB {project.mingw.get_ranlib(platform)}) +set(CMAKE_RC_COMPILER {project.mingw.get_windres(platform)}) set(CMAKE_SYSTEM_NAME Windows) ''' @staticmethod - @contextmanager def setup(platform, build_dir): - platform = platform or Platform.native() - path = File._get_path(build_dir) - with open(path, mode='w') as file: - file.write(MinGW._format(platform)) - yield MinGW(path) + return MinGW.write_config(build_dir, MinGW._format(platform)) + + +class Clang(Makefile): + @staticmethod + def _format(platform): + return f''' +if(CMAKE_VERSION VERSION_LESS "3.15" AND WIN32) + set(CMAKE_C_COMPILER clang-cl) + set(CMAKE_CXX_COMPILER clang-cl) + set(CMAKE_C_FLAGS -m{platform.get_address_model()}) + set(CMAKE_CXX_FLAGS -m{platform.get_address_model()}) +else() + set(CMAKE_C_COMPILER clang) + set(CMAKE_CXX_COMPILER clang++) + set(CMAKE_C_FLAGS -m{platform.get_address_model()}) + set(CMAKE_CXX_FLAGS -m{platform.get_address_model()}) +endif() +''' + + def _get_makefile_generator(self): + if on_windows(): + # MinGW utilities like make might be unavailable, but NMake can + # very much be there. + if shutil.which('nmake'): + return 'NMake Makefiles' + return super()._get_makefile_generator() + + @staticmethod + def setup(platform, build_dir): + return Clang.write_config(build_dir, Clang._format(platform)) + + +class ClangCL(Clang): + @staticmethod + def _format(platform): + return f''' +set(CMAKE_C_COMPILER clang-cl) +set(CMAKE_CXX_COMPILER clang-cl) +set(CMAKE_C_FLAGS -m{platform.get_address_model()}) +set(CMAKE_CXX_FLAGS -m{platform.get_address_model()}) +set(CMAKE_SYSTEM_NAME Windows) +''' + + @staticmethod + def setup(platform, build_dir): + return ClangCL.write_config(build_dir, ClangCL._format(platform)) diff --git a/project/mingw.py b/project/mingw.py index 1e136cd..731cee9 100644 --- a/project/mingw.py +++ b/project/mingw.py @@ -3,8 +3,6 @@ # For details, see https://github.com/egor-tensin/cmake-common. # Distributed under the MIT License. -from project.os import on_windows_like - def _get_compiler_prefix(platform): target_arch = platform.get_address_model() @@ -17,11 +15,7 @@ def _get_compiler_prefix(platform): def _get(platform, what): prefix = _get_compiler_prefix(platform) - ext = '' - if on_windows_like(): - # Boost.Build wants the .exe extension at the end on Cygwin. - ext = '.exe' - path = f'{prefix}-w64-mingw32-{what}{ext}' + path = f'{prefix}-w64-mingw32-{what}' return path @@ -33,5 +27,13 @@ def get_gxx(platform): return _get(platform, 'g++') +def get_ar(platform): + return _get(platform, 'gcc-ar') + + +def get_ranlib(platform): + return _get(platform, 'gcc-ranlib') + + def get_windres(platform): return _get(platform, 'windres') diff --git a/project/toolchain.py b/project/toolchain.py new file mode 100644 index 0000000..d931c6b --- /dev/null +++ b/project/toolchain.py @@ -0,0 +1,45 @@ +# Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com> +# This file is part of the "cmake-common" project. +# For details, see https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +'''Supported platform/build system/compiler combinations include, but are not +limited to: + +* Linux / make / Clang, +* Linux / make / GCC, +* Linux / make / MinGW-w64, +* Windows / make / Clang (clang.exe & clang++.exe), +* Windows / make / Clang (clang-cl.exe, Boost 1.69.0 or higher only), +* Windows / make / MinGW-w64, +* Windows / msbuild / MSVC, +* Cygwin / make / Clang, +* Cygwin / make / GCC, +* Cygwin / make / MinGW-w64. +''' + +import argparse +from enum import Enum + + +class ToolchainType(Enum): + AUTO = 'auto' # This most commonly means GCC on Linux and MSVC on Windows. + MSVC = 'msvc' # Force MSVC. + GCC = 'gcc' # Force GCC. + MINGW = 'mingw' # As in MinGW-w64; GCC with the PLATFORM-w64-mingw32 prefix. + CLANG = 'clang' + CLANG_CL = 'clang-cl' + + def __str__(self): + return self.value + + @staticmethod + def all(): + return tuple(ToolchainType) + + @staticmethod + def parse(s): + try: + return ToolchainType(s) + except ValueError: + raise argparse.ArgumentTypeError(f'invalid toolset: {s}') |