aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rwxr-xr-x.ci/verify_symbols.sh2
-rw-r--r--.github/actions/build-boost/action.yml53
-rw-r--r--.github/actions/build-example/action.yml69
-rw-r--r--.github/actions/check-arch/action.yml23
-rw-r--r--.github/actions/check-boost-bootstrapped/action.yml44
-rw-r--r--.github/actions/check-boost-libraries/action.yml77
-rw-r--r--.github/actions/check-runtime-library/action.yml161
-rw-r--r--.github/actions/check-symbols/action.yml46
-rw-r--r--.github/actions/common-variables/action.yml85
-rw-r--r--.github/actions/download-boost/action.yml40
-rw-r--r--.github/actions/run-example-boost/action.yml45
-rw-r--r--.github/actions/run-example/action.yml28
-rw-r--r--.github/actions/software-environment/action.yml63
-rw-r--r--.github/workflows/boost_clang_windows.yml143
-rw-r--r--.github/workflows/boost_toolsets.yml222
-rw-r--r--.github/workflows/cygwin_static_libstdc++.yml81
-rw-r--r--.github/workflows/example_toolsets.yml168
-rw-r--r--common.cmake144
-rw-r--r--examples/boost/foo.cpp10
-rw-r--r--examples/dynamic/foo.cpp3
-rw-r--r--examples/static/foo.cpp3
-rw-r--r--project/boost/build.py34
-rw-r--r--project/boost/directory.py18
-rw-r--r--project/boost/download.py5
-rw-r--r--project/boost/toolchain.py455
-rw-r--r--project/ci/boost.py11
-rw-r--r--project/ci/cmake.py8
-rw-r--r--project/cmake/build.py53
-rw-r--r--project/cmake/toolchain.py281
-rw-r--r--project/mingw.py16
-rw-r--r--project/toolchain.py45
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}')