From d1bdbbef08c5d1778aac57b8f1f6007940c03c27 Mon Sep 17 00:00:00 2001 From: Paul Meems Date: Sat, 31 Oct 2026 10:31:17 +0100 Subject: [PATCH 1/3] Audit the MSBuild build and add CMake parity tooling Co-authored-by: Junie --- .gitignore | 7 +- build/scripts/Compare-CompileCommands.ps1 | 514 ++++++++++++++++++++++ docs/BUILDING.md | 12 + docs/CMAKE-MIGRATION.md | 304 +++++++++++++ 4 files changed, 836 insertions(+), 1 deletion(-) create mode 100644 build/scripts/Compare-CompileCommands.ps1 create mode 100644 docs/CMAKE-MIGRATION.md diff --git a/.gitignore b/.gitignore index 1f51f99..d195f40 100644 --- a/.gitignore +++ b/.gitignore @@ -428,8 +428,13 @@ FodyWeavers.xsd *.msm *.msp -# vcpkg installed libraries +# vcpkg installed libraries (MSBuild: vcpkg_installed/, CMake: vcpkg_installed/cmake-) vcpkg_installed/ +# CMake build trees, CLion build directories and machine-specific presets +out/ +cmake-build-*/ +CMakeUserPresets.json + # C++ language server compilation database .mscppls/ diff --git a/build/scripts/Compare-CompileCommands.ps1 b/build/scripts/Compare-CompileCommands.ps1 new file mode 100644 index 0000000..8fdc61f --- /dev/null +++ b/build/scripts/Compare-CompileCommands.ps1 @@ -0,0 +1,514 @@ +<# +.SYNOPSIS + Measures compile, link and binary parity between the MSBuild and CMake builds. + +.DESCRIPTION + Collects the effective compiler and linker command lines of a build, + normalises them (paths relative to the repository, build-directory paths + replaced by placeholders, option order sorted) and writes a JSON report. + + The MSBuild side is read from a detailed MSBuild file log. Without + -MSBuildLog, the script builds MapWinGIS6.slnx itself and records that log. + The CMake side is optional: pass -CMakeBuildDir to read its + compile_commands.json and, with -CMakeBuildLog, the link lines of a verbose + build (`cmake --build -- -v`). When both sides are present, each + translation unit and each linked binary is diffed and every option that only + one build uses is reported. + + The report also contains the warning count of each build and, with + -Binaries, a dumpbin summary (dependents, exports, subsystem, DLL + characteristics) of each binary in both output folders. + + MSBuild is located in this order: -MSBuild, the MWG_MSBUILD environment + variable, msbuild on PATH, then vswhere. + +.EXAMPLE + pwsh -File build\scripts\Compare-CompileCommands.ps1 -Configuration Debug -Platform x64 + + Builds the solution with MSBuild and writes the baseline report. + +.EXAMPLE + pwsh -File build\scripts\Compare-CompileCommands.ps1 -MSBuildLog out\parity\msbuild-Debug-x64.log ` + -CMakeBuildDir out\b\x64d -CMakeBuildLog out\parity\cmake-x64d.log -Binaries MapWinGIS.Core.dll + + Compares an existing MSBuild log with a CMake Ninja build. +#> +[CmdletBinding()] +param( + [ValidateSet('Debug', 'Release')] + [string] $Configuration = 'Debug', + + [ValidateSet('x64', 'Win32')] + [string] $Platform = 'x64', + + [string] $MSBuild, + + [string] $MSBuildLog, + + [string] $MSBuildBinDirectory, + + [string] $CMakeBuildDir, + + [string] $CMakeBuildLog, + + [string] $CMakeBinDirectory, + + [string[]] $Binaries = @(), + + [string] $OutputPath, + + [switch] $FailOnLostFlags +) + +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$parityDir = Join-Path $repoRoot 'out\parity' + +# Options that only control logging, parallelism or object/PDB file names, so +# they say nothing about the generated code. +$ignoredOptions = @( + '/c', '/nologo', '/MP', '/FC', '/errorReport:queue', '/ERRORREPORT:QUEUE', '/NOLOGO', + '/diagnostics:column', '/diagnostics:caret', '/showIncludes', '/FS' +) +$ignoredOptionPrefixes = @('/Fo', '/Fd', '/Fe', '/Fp', '/ILK:', '/PDB:', '/IMPLIB:', '/OUT:', '/MP') + +# MSBuild project names that differ from the CMake target names. +$targetNames = @{ 'Translate' = 'MapWinGIS.Samples.Translate' } + +function Find-MSBuild { + if ($MSBuild) { return $MSBuild } + if ($env:MWG_MSBUILD) { return $env:MWG_MSBUILD } + + $onPath = Get-Command msbuild -ErrorAction SilentlyContinue + if ($onPath) { return $onPath.Source } + + $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + if (Test-Path -LiteralPath $vswhere) { + $found = & $vswhere -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | + Select-Object -First 1 + if ($found) { return $found } + } + + throw 'MSBuild was not found. Set MWG_MSBUILD, pass -MSBuild, or run from a Developer PowerShell.' +} + +function Find-Dumpbin { + $onPath = Get-Command dumpbin -ErrorAction SilentlyContinue + if ($onPath) { return $onPath.Source } + + $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + if (Test-Path -LiteralPath $vswhere) { + $found = & $vswhere -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` + -find 'VC\Tools\MSVC\**\bin\Hostx64\x64\dumpbin.exe' | Select-Object -First 1 + if ($found) { return $found } + } + return $null +} + +# Splits a command line the way the MSVC tools do: whitespace separates +# arguments, double quotes group them, and \" is a literal quote. +function Split-CommandLine([string] $line) { + $tokens = [System.Collections.Generic.List[string]]::new() + $current = [System.Text.StringBuilder]::new() + $inQuotes = $false + $hasToken = $false + for ($i = 0; $i -lt $line.Length; $i++) { + $ch = $line[$i] + if ($ch -eq '\') { + # 2n backslashes before a quote are n backslashes and a delimiter, + # 2n+1 are n backslashes and a literal quote; otherwise they are literal. + $count = 0 + while ($i -lt $line.Length -and $line[$i] -eq '\') { $count++; $i++ } + if ($i -lt $line.Length -and $line[$i] -eq '"') { + [void]$current.Append([char]'\', [int][Math]::Floor($count / 2)) + if ($count % 2 -eq 1) { [void]$current.Append('"') } else { $inQuotes = -not $inQuotes } + } + else { + [void]$current.Append([char]'\', $count) + $i-- + } + $hasToken = $true + continue + } + if ($ch -eq '"') { $inQuotes = -not $inQuotes; $hasToken = $true; continue } + if (-not $inQuotes -and [char]::IsWhiteSpace($ch)) { + if ($hasToken) { $tokens.Add($current.ToString()); [void]$current.Clear(); $hasToken = $false } + continue + } + [void]$current.Append($ch); $hasToken = $true + } + if ($hasToken) { $tokens.Add($current.ToString()) } + return $tokens +} + +function Get-NormalizedPath([string] $path, [string] $baseDir, [hashtable] $placeholders) { + $path = $path.Trim().TrimEnd('\', '/') + if (-not [System.IO.Path]::IsPathRooted($path) -and $baseDir) { + $path = Join-Path $baseDir $path + } + try { $path = [System.IO.Path]::GetFullPath($path) } catch { } + $path = $path.TrimEnd('\', '/') + + # Longest prefix first, so build directories win over the repository root. + foreach ($key in ($placeholders.Keys | Sort-Object Length -Descending)) { + if ($path.StartsWith($key, [System.StringComparison]::OrdinalIgnoreCase)) { + $rest = $path.Substring($key.Length).TrimStart('\', '/') + $value = $placeholders[$key] + return $(if ($rest) { "$value/$($rest -replace '\\', '/')" } else { $value }) + } + } + return ($path -replace '\\', '/') +} + +# Turns raw arguments into a sorted set of normalised options. Options with a +# separate argument (/D X, /I X) are joined, '-' is mapped to '/', and paths +# become placeholders such as , , or . +function ConvertTo-OptionSet([string[]] $arguments, [string] $baseDir, [hashtable] $placeholders, + [string[]] $sourceExtensions) { + $options = [System.Collections.Generic.List[string]]::new() + $sources = [System.Collections.Generic.List[string]]::new() + # Compiler and linker share the argument list here, so each prefix only + # applies to the tool that owns it (/I is a compiler option, /INCREMENTAL is not a path). + $isLink = $sourceExtensions -contains '.obj' + $pathOptions = if ($isLink) { @('/LIBPATH:', '/DEF:', '/MANIFESTINPUT:', '/WHOLEARCHIVE:') } + else { @('/I', '/external:I', '/FI') } + $joined = if ($isLink) { @() } else { @('/D', '/I', '/U', '/external:I', '/FI') } + + for ($i = 0; $i -lt $arguments.Count; $i++) { + $arg = $arguments[$i] + if ($arg.StartsWith('-')) { $arg = '/' + $arg.Substring(1) } + if ($arg.StartsWith('@')) { continue } + + if ($joined -contains $arg -and $i + 1 -lt $arguments.Count) { + $arg = $arg + $arguments[++$i] + } + + if (-not $arg.StartsWith('/')) { + $extension = [System.IO.Path]::GetExtension($arg) + if ($sourceExtensions -contains $extension) { + $sources.Add((Get-NormalizedPath $arg $baseDir $placeholders)) + } + elseif ($extension -eq '.lib' -or $arg.EndsWith('*.lib')) { + $name = if ([System.IO.Path]::IsPathRooted($arg)) { + Get-NormalizedPath $arg $baseDir $placeholders + } else { $arg.ToLowerInvariant() } + $options.Add("lib:$name") + } + elseif ($extension -in @('.res', '.def')) { + $options.Add("input:$([System.IO.Path]::GetFileName($arg))") + } + continue + } + + if ($ignoredOptions -contains $arg) { continue } + $skip = $false + foreach ($prefix in $ignoredOptionPrefixes) { + if ($arg.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { $skip = $true; break } + } + if ($skip) { continue } + + foreach ($prefix in $pathOptions) { + if ($arg.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase) -and $arg.Length -gt $prefix.Length) { + $arg = $prefix + (Get-NormalizedPath $arg.Substring($prefix.Length) $baseDir $placeholders) + break + } + } + $options.Add($arg) + } + + return [pscustomobject]@{ + Options = @($options | Sort-Object -Unique -CaseSensitive) + Sources = @($sources) + } +} + +function Get-Placeholders([string] $intRoot, [string] $binDir, [string] $buildDir) { + $map = @{} + $map[$repoRoot.TrimEnd('\')] = '' + foreach ($installed in @('vcpkg_installed')) { + $dir = Join-Path $repoRoot $installed + $map[$dir] = '' + } + if ($buildDir) { + $map[$buildDir.TrimEnd('\')] = '' + $map[(Join-Path $buildDir 'vcpkg_installed')] = '' + } + if ($intRoot) { $map[$intRoot.TrimEnd('\')] = '' } + if ($binDir) { $map[$binDir.TrimEnd('\')] = '' } + return $map +} + +# vcpkg install trees differ between the builds (vcpkg_installed\x64 for +# MSBuild, vcpkg_installed\cmake-x64 or similar for CMake). Collapse the +# platform folder so both report //... +function Merge-VcpkgPlatform([string[]] $options) { + return @($options | ForEach-Object { + $_ -replace '/(cmake-)?(x64|Win32|x86)/', '/' + } | Sort-Object -Unique -CaseSensitive) +} + +function Read-MSBuildCommands([string] $logPath, [string] $binDir) { + $compiles = @{} + $links = @{} + $projectDirs = @{} + Get-ChildItem -Path $repoRoot -Recurse -Filter *.vcxproj -File | + Where-Object { $_.FullName -notmatch '\\(out|obj|bin|vcpkg_installed)\\' } | + ForEach-Object { $projectDirs[$_.BaseName] = $_.DirectoryName } + + $objRoot = Join-Path $repoRoot 'obj' + foreach ($line in [System.IO.File]::ReadLines($logPath)) { + $match = [regex]::Match($line, '^\s*(?[A-Za-z]:\\.*?\\(?CL|link)\.exe)\s+(?.*)$', + [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + if (-not $match.Success) { continue } + + $arguments = Split-CommandLine $match.Groups['args'].Value + $tool = $match.Groups['tool'].Value.ToLowerInvariant() + + if ($tool -eq 'cl') { + $fo = $arguments | Where-Object { $_ -match '^[/-]Fo' } | Select-Object -First 1 + if (-not $fo) { continue } + $intDir = $fo.Substring(3).TrimEnd('\') + $project = ($intDir.Substring($objRoot.Length).TrimStart('\') -split '\\')[0] + $baseDir = $projectDirs[$project] + $target = if ($targetNames.ContainsKey($project)) { $targetNames[$project] } else { $project } + $placeholders = Get-Placeholders $intDir $binDir $null + $set = ConvertTo-OptionSet $arguments $baseDir $placeholders @('.c', '.cpp', '.cxx', '.cc') + foreach ($source in $set.Sources) { + # Keyed by target: the tests compile some ActiveX sources a second time. + $compiles["$target $source"] = [pscustomobject]@{ + Target = $target + Source = $source + Options = Merge-VcpkgPlatform $set.Options + } + } + } + else { + $out = $arguments | Where-Object { $_ -match '^[/-]OUT:' } | Select-Object -First 1 + if (-not $out) { continue } + $binary = [System.IO.Path]::GetFileName($out.Substring(5)) + # Object files are logged on separate lines; any argument inside obj\\ names the project. + $intFile = $arguments | ForEach-Object { $_ -replace '^[/-][A-Za-z]+:', '' } | + Where-Object { $_.StartsWith($objRoot, [System.StringComparison]::OrdinalIgnoreCase) } | + Select-Object -First 1 + $intDir = if ($intFile) { Split-Path $intFile -Parent } else { $null } + $project = if ($intDir) { ($intDir.Substring($objRoot.Length).TrimStart('\') -split '\\')[0] } else { $null } + $baseDir = if ($project) { $projectDirs[$project] } else { $null } + $placeholders = Get-Placeholders $intDir $binDir $null + $set = ConvertTo-OptionSet $arguments $baseDir $placeholders @('.obj') + $links[$binary] = Merge-VcpkgPlatform $set.Options + } + } + + return [pscustomobject]@{ Compiles = $compiles; Links = $links } +} + +function Read-CMakeCommands([string] $buildDir, [string] $logPath, [string] $binDir) { + $compiles = @{} + $links = @{} + + $database = Join-Path $buildDir 'compile_commands.json' + if (-not (Test-Path -LiteralPath $database)) { + throw "No compile_commands.json in $buildDir. Configure with CMAKE_EXPORT_COMPILE_COMMANDS=ON (Ninja generator)." + } + + foreach ($entry in (Get-Content -Raw -LiteralPath $database | ConvertFrom-Json)) { + $arguments = if ($entry.arguments) { @($entry.arguments | Select-Object -Skip 1) } + else { @(Split-CommandLine $entry.command | Select-Object -Skip 1) } + $fo = $arguments | Where-Object { $_ -match '^[/-]Fo' } | Select-Object -First 1 + $intDir = if ($fo) { Split-Path ([System.IO.Path]::GetFullPath((Join-Path $entry.directory $fo.Substring(3)))) -Parent } else { $null } + $placeholders = Get-Placeholders $intDir $binDir $buildDir + $set = ConvertTo-OptionSet $arguments $entry.directory $placeholders @('.c', '.cpp', '.cxx', '.cc') + $source = Get-NormalizedPath $entry.file $entry.directory $placeholders + $target = if ($intDir -match '\\CMakeFiles\\(?[^\\]+)\.dir') { $Matches['t'] } else { '' } + $compiles["$target $source"] = [pscustomobject]@{ + Target = $target + Source = $source + Options = Merge-VcpkgPlatform $set.Options + } + } + + if ($logPath) { + foreach ($line in [System.IO.File]::ReadLines($logPath)) { + # Ninja wraps links in "cmake -E vs_link_dll|vs_link_exe ... -- link.exe ..." + $match = [regex]::Match($line, '(?:^|\s|")(?[^\s"]*link\.exe"?)\s+(?.*)$', + [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + if (-not $match.Success) { continue } + $arguments = Split-CommandLine $match.Groups['args'].Value + $out = $arguments | Where-Object { $_ -match '^[/-]out:' } | Select-Object -First 1 + if (-not $out) { continue } + $binary = [System.IO.Path]::GetFileName($out.Substring(5)) + $placeholders = Get-Placeholders $null $binDir $buildDir + $set = ConvertTo-OptionSet $arguments $buildDir $placeholders @('.obj') + $links[$binary] = Merge-VcpkgPlatform $set.Options + } + } + + return [pscustomobject]@{ Compiles = $compiles; Links = $links } +} + +function Get-WarningCount([string] $logPath) { + if (-not $logPath -or -not (Test-Path -LiteralPath $logPath)) { return $null } + $unique = [System.Collections.Generic.HashSet[string]]::new() + foreach ($line in [System.IO.File]::ReadLines($logPath)) { + $match = [regex]::Match($line, '(?[^\s>][^>]*?\(\d+(?:,\d+)?\))?\s*:\s*warning\s+(?[A-Z]+\d+)\s*:(?[^\[]*)') + if ($match.Success) { + # Detailed logs repeat each warning in the project summary, so count unique ones. + [void]$unique.Add(($match.Groups['loc'].Value.Trim() + '|' + $match.Groups['code'].Value + '|' + $match.Groups['msg'].Value.Trim())) + } + } + return $unique.Count +} + +function Get-BinarySummary([string] $dumpbin, [string] $path) { + if (-not (Test-Path -LiteralPath $path)) { return $null } + + $dependents = @(& $dumpbin /nologo /dependents $path | + Where-Object { $_ -match '^\s+\S+\.(dll|DLL|ocx|exe)\s*$' } | + ForEach-Object { $_.Trim().ToLowerInvariant() } | Sort-Object -Unique) + + $exports = @(& $dumpbin /nologo /exports $path | + ForEach-Object { + $m = [regex]::Match($_, '^\s+\d+\s+[0-9A-F]+\s+(?:[0-9A-F]{8}\s+)?(?\S+)') + if ($m.Success) { $m.Groups['name'].Value } + } | Sort-Object -Unique -CaseSensitive) + + $headers = & $dumpbin /nologo /headers $path + $subsystem = ($headers | Where-Object { $_ -match 'subsystem \(' } | Select-Object -First 1) + $characteristics = @($headers | + Where-Object { $_ -match '^\s+(Dynamic base|NX compatible|High Entropy Virtual Addresses|Control Flow Guard|Guard|Terminal Server Aware|Application can handle large)' } | + ForEach-Object { $_.Trim() } | Sort-Object -Unique) + + return [pscustomobject]@{ + Dependents = $dependents + Exports = $exports + Subsystem = if ($subsystem) { $subsystem.Trim() } else { $null } + DllCharacteristics = $characteristics + } +} + +function Compare-OptionSets($reference, $actual) { + $lost = @($reference | Where-Object { $actual -cnotcontains $_ }) + $added = @($actual | Where-Object { $reference -cnotcontains $_ }) + return [pscustomobject]@{ Lost = $lost; Added = $added } +} + +# --- MSBuild baseline ------------------------------------------------------ + +if (-not $MSBuildBinDirectory) { + $MSBuildBinDirectory = Join-Path $repoRoot "bin\$Configuration\$Platform" +} + +if (-not $MSBuildLog) { + New-Item -ItemType Directory -Force -Path $parityDir | Out-Null + $MSBuildLog = Join-Path $parityDir "msbuild-$Configuration-$Platform.log" + $msbuildExe = Find-MSBuild + Write-Host "MSBuild: $msbuildExe" + # A rebuild guarantees that every translation unit appears in the log. + & $msbuildExe (Join-Path $repoRoot 'MapWinGIS6.slnx') -restore '-t:Rebuild' "-p:Configuration=$Configuration" ` + "-p:Platform=$Platform" -m '-v:m' "-flp:logfile=$MSBuildLog;verbosity=detailed" + if ($LASTEXITCODE -ne 0) { throw "MSBuild failed with exit code $LASTEXITCODE" } +} + +$MSBuildLog = (Resolve-Path -LiteralPath $MSBuildLog).Path +$msbuildCommands = Read-MSBuildCommands $MSBuildLog $MSBuildBinDirectory + +$report = [ordered]@{ + configuration = $Configuration + platform = $Platform + msbuild = [ordered]@{ + log = $MSBuildLog + warnings = Get-WarningCount $MSBuildLog + compiles = $msbuildCommands.Compiles + links = $msbuildCommands.Links + } +} + +# --- CMake side and diff --------------------------------------------------- + +$lostTotal = 0 +if ($CMakeBuildDir) { + $CMakeBuildDir = (Resolve-Path -LiteralPath $CMakeBuildDir).Path + if (-not $CMakeBinDirectory) { $CMakeBinDirectory = Join-Path $CMakeBuildDir 'bin' } + if ($CMakeBuildLog) { $CMakeBuildLog = (Resolve-Path -LiteralPath $CMakeBuildLog).Path } + $cmakeCommands = Read-CMakeCommands $CMakeBuildDir $CMakeBuildLog $CMakeBinDirectory + + $compileDiff = [ordered]@{} + # Targets that CMake does not build yet are skipped, so one phase can be + # measured before the later targets exist. + $cmakeTargets = @($cmakeCommands.Compiles.Values | ForEach-Object Target | Sort-Object -Unique) + foreach ($key in ($msbuildCommands.Compiles.Keys | Sort-Object)) { + $reference = $msbuildCommands.Compiles[$key] + if ($cmakeTargets -notcontains $reference.Target) { continue } + $actual = $cmakeCommands.Compiles[$key] + if (-not $actual) { + $compileDiff[$key] = 'missing in CMake' + $lostTotal++ + continue + } + $diff = Compare-OptionSets $reference.Options $actual.Options + if ($diff.Lost.Count -or $diff.Added.Count) { + $compileDiff[$key] = $diff + $lostTotal += $diff.Lost.Count + } + } + foreach ($key in ($cmakeCommands.Compiles.Keys | Where-Object { -not $msbuildCommands.Compiles.ContainsKey($_) })) { + $compileDiff[$key] = 'only in CMake' + } + + $linkDiff = [ordered]@{} + foreach ($binary in ($msbuildCommands.Links.Keys | Sort-Object)) { + if (-not $cmakeCommands.Links.ContainsKey($binary)) { continue } + $diff = Compare-OptionSets $msbuildCommands.Links[$binary] $cmakeCommands.Links[$binary] + if ($diff.Lost.Count -or $diff.Added.Count) { + $linkDiff[$binary] = $diff + $lostTotal += $diff.Lost.Count + } + } + + $report.cmake = [ordered]@{ + buildDir = $CMakeBuildDir + log = $CMakeBuildLog + warnings = Get-WarningCount $CMakeBuildLog + compiles = $cmakeCommands.Compiles + links = $cmakeCommands.Links + } + $report.diff = [ordered]@{ compiles = $compileDiff; links = $linkDiff } +} + +# --- Binaries -------------------------------------------------------------- + +# pwsh -File passes "a,b" as one string. +$Binaries = @($Binaries | ForEach-Object { $_ -split ',' } | ForEach-Object Trim | Where-Object { $_ }) +if ($Binaries.Count) { + $dumpbin = Find-Dumpbin + if (-not $dumpbin) { throw 'dumpbin.exe was not found; install the MSVC build tools or run from a Developer PowerShell.' } + $binaryReport = [ordered]@{} + foreach ($binary in $Binaries) { + $entry = [ordered]@{ msbuild = Get-BinarySummary $dumpbin (Join-Path $MSBuildBinDirectory $binary) } + if ($CMakeBuildDir) { $entry.cmake = Get-BinarySummary $dumpbin (Join-Path $CMakeBinDirectory $binary) } + $binaryReport[$binary] = $entry + } + $report.binaries = $binaryReport +} + +# --- Output ---------------------------------------------------------------- + +if (-not $OutputPath) { + New-Item -ItemType Directory -Force -Path $parityDir | Out-Null + $OutputPath = Join-Path $parityDir "parity-$Configuration-$Platform.json" +} +$report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $OutputPath -Encoding utf8 + +Write-Host '' +Write-Host "Configuration: $Configuration|$Platform" +Write-Host "MSBuild TUs/links: $($msbuildCommands.Compiles.Count) / $($msbuildCommands.Links.Count)" +Write-Host "MSBuild warnings: $($report.msbuild.warnings)" +if ($CMakeBuildDir) { + Write-Host "CMake TUs/links: $($report.cmake.compiles.Count) / $($report.cmake.links.Count)" + Write-Host "CMake warnings: $($report.cmake.warnings)" + Write-Host "Lost options: $lostTotal" +} +Write-Host "Report: $OutputPath" + +if ($FailOnLostFlags -and $lostTotal -gt 0) { exit 1 } diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 3ccaec5..53a2fa9 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -189,6 +189,18 @@ bin\Release\x64\MapWinGIS.Core.Tests.exe --gtest_output=xml:testresults.xml | `MwgEnableAsan` | `false` | Adds `/fsanitize=address` | | `MwgDeployRuntimeData` | `true` | Copies `gdal-data` and `proj` to the output folder | +## CMake migration + +CMake is being introduced as the primary native build. Until it is complete, +the MSBuild solution above stays the supported build. The audit of the current +MSBuild settings and the decisions the CMake build follows are in +[CMAKE-MIGRATION.md](CMAKE-MIGRATION.md). + +To measure flag and binary parity, `build\scripts\Compare-CompileCommands.ps1` +records a detailed MSBuild log and writes a normalised report to `out\parity\`. +It finds MSBuild through `MWG_MSBUILD`, `PATH` or `vswhere`, so it also works +when Visual Studio is not on `PATH` or not installed on `C:`. + ## Troubleshooting **`Invalid triplet: x64-windows-v145`** diff --git a/docs/CMAKE-MIGRATION.md b/docs/CMAKE-MIGRATION.md new file mode 100644 index 0000000..89e6eb6 --- /dev/null +++ b/docs/CMAKE-MIGRATION.md @@ -0,0 +1,304 @@ +# CMake migration: audit and decisions + +This document records what the MSBuild build actually does today and the +decisions that the CMake build follows. It is the reference for every CMake +phase: when a CMake target differs from the table here, the difference must be +deliberate and explained in the PR. + +The figures below come from the MSBuild baseline (`Debug|x64` and +`Release|x64`, VS 2026 18.11 MSBuild, MSVC 14.51, Windows SDK 10.0.28000.0), +collected with [`build/scripts/Compare-CompileCommands.ps1`](#parity-measurement-d6). + +## Targets + +| MSBuild project | Output | Type | Planned CMake target | +|---|---|---|---| +| `src/MapWinGIS.Core/MapWinGIS.Core.vcxproj` | `MapWinGIS.Core.dll` (+ `.lib`, `.pdb`) | DLL, subsystem `WINDOWS` | `MapWinGIS.Core` | +| `src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.vcxproj` | `MapWinGIS.ocx` (+ `.lib`, `.pdb`, `MapWinGIS.ActiveX.tlb`) | DLL, subsystem `WINDOWS`, `UseOfAtl=Static` | `MapWinGIS.ActiveX.Interface` (MIDL) + `MapWinGIS.ActiveX` | +| `tests/MapWinGIS.Core.Tests/MapWinGIS.Core.Tests.vcxproj` | `MapWinGIS.Core.Tests.exe` | Console | `MapWinGIS.Core.Tests` | +| `samples/Cpp/Translate.vcxproj` (project name `TranslateSample`) | `translate.exe` | Console | `MapWinGIS.Samples.Translate` (`OUTPUT_NAME translate`) | +| `samples/CSharp/ActiveXTestHost/ActiveXTestHost.csproj` | `ActiveXTestHost.exe` | .NET 10 WinForms | custom targets around `dotnet build` | + +All native projects build `Debug` and `Release` for `x64` and `Win32`. + +### Sources + +**MapWinGIS.Core** — `MWG_CORE_BUILD`; include directories `include` (public) +and `src` (private). + +- Public headers: `include/mapwingis/core/{Api,Enums,Error,Extent}.h`, + `include/mapwingis/gdal/{GdalEnvironment,GdalConfig,GdalDiagnostics,GdalProgress,GdalOperationResult,GdalAlgorithmRunner,GdalTypedOptions,GdalModern,GdalUtils}.h`, + `include/mapwingis/vector/{FieldInfo,VectorDataset}.h`, + `include/mapwingis/rendering/VectorRenderScene.h`. +- Private headers: `src/internal/{GdalExecution,GdalProgressAdapterAccess,Utf8,VectorLayerAccess}.h`. +- Sources: `src/gdal/{GdalAlgorithmRunner,GdalEnvironment,GdalModern,GdalOperationResult,GdalRuntime,GdalTypedOptions,GdalUtils}.cpp`, + `src/vector/VectorDataset.cpp`, `src/rendering/VectorRenderScene.cpp`. +- No public header includes a GDAL, GEOS or Windows header, so GDAL and GEOS + can be `PRIVATE` dependencies of the DLL. + +**MapWinGIS.ActiveX** — `_ATL_APARTMENT_THREADED`; include directories +`src/MapWinGIS.Core/include` and the MIDL output directory (`$(IntDir)`). + +- Headers: `ActiveXModule.h`, `ComConversion.h`, `GdalDispatchWindow.h` + (header-only, not listed in the vcxproj), `GdalEventSource.h`, + `GdalOperationDispatcher.h`, `GdiVectorLayerRenderer.h`, + `MapWinGISControl.h`, `OpenGlVectorLayerRenderer.h`, `VectorLayerRenderer.h`, + `VectorRenderBackend.h`, `ViewTransform.h`, `resource.h`. +- Sources: `ActiveXModule.cpp`, `ComConversion.cpp`, + `GdalOperationDispatcher.cpp`, `GdalEventSource.cpp`, + `GdiVectorLayerRenderer.cpp`, `MapWinGISControl.cpp`, + `OpenGlVectorLayerRenderer.cpp`, `VectorLayerRenderer.cpp`, + `ViewTransform.cpp`, plus the generated `MapWinGIS.ActiveX_i.c` (compiled as + C, `/std:c17 /TC`). +- Other inputs: `MapWinGIS.ActiveX.idl` (MIDL), `MapWinGIS.ActiveX.rc` + (embeds `MapWinGISControl.rgs` as `REGISTRY` and the type library as + `1 TYPELIB "MapWinGIS.ActiveX.tlb"`), `MapWinGIS.ActiveX.def` (linker + `/DEF`, exports `DllCanUnloadNow`, `DllGetClassObject`, `DllRegisterServer`, + `DllUnregisterServer`, `DllInstall`). +- No `.manifest` file; the linker embeds its default manifest. + +**MapWinGIS.Core.Tests** — `MWG_ENABLE_RENDERER_TEST_HOOKS`; include +directories `src/MapWinGIS.Core/include`, `src/MapWinGIS.ActiveX` and the +ActiveX MIDL output directory `obj/MapWinGIS.ActiveX//`. + +- Headers: `TestData.h`. +- Sources: `ActiveXActivationTests.cpp`, `ComConversionTests.cpp`, + `GdalAlgorithmRunnerTests.cpp`, `GdalEnvironmentTests.cpp`, + `GdalModernCapabilitiesTests.cpp`, `GdalOperationDispatcherTests.cpp`, + `GdalRuntimeTests.cpp`, `GdalTypedOptionsTests.cpp`, `GdalUtilsTests.cpp`, + `Main.cpp` (defines `main`), `RenderingBaselineTests.cpp`, `TestData.cpp`, + `VectorDatasetTests.cpp`, `VectorLayerRendererTests.cpp`, + `VectorLayerTests.cpp`, `VectorRenderSceneTests.cpp`. +- ActiveX sources compiled again with the test hook definition: + `GdiVectorLayerRenderer.cpp`, `OpenGlVectorLayerRenderer.cpp`, + `GdalOperationDispatcher.cpp`, `ComConversion.cpp`, + `VectorLayerRenderer.cpp`, `ViewTransform.cpp`. +- Project references: Core (linked) and ActiveX (build order only, + `LinkLibraryDependencies=false`), so the OCX is beside the test executable. + +**Translate sample** — `samples/Cpp/translate.cpp` (`wmain`); include +directory `src/MapWinGIS.Core/include`. It uses only the Core public API. + +## Effective compiler options + +Common to every C/C++ translation unit (from `build/props/MapWinGIS.Cpp.props` +and MSBuild defaults): + +| Option | Source | +|---|---| +| `/W4 /WX- /wd4251 /external:W4` | `WarningLevel`, `DisableSpecificWarnings`; `/external:W4` is the MSBuild default for external headers | +| `/utf-8 /Zc:__cplusplus /Zc:preprocessor /Zc:throwingNew /bigobj` | `AdditionalOptions` | +| `/permissive- /sdl /EHsc /GS /Gd /Gm- /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline` | `ConformanceMode`, `SDLCheck`, `ExceptionHandling`, MSBuild defaults | +| `/std:c++20` (`/TP`), `/std:c17` (`/TC`, `_i.c` only) | `LanguageStandard`, `LanguageStandard_C` | +| `/Zi` | `DebugInformationFormat=ProgramDatabase`, **Debug and Release** | +| `/D UNICODE /D _UNICODE` | `CharacterSet=Unicode` | +| `/D WIN32_LEAN_AND_MEAN /D NOMINMAX /D _WIN32_WINNT=0x0A00 /D WINVER=0x0A00 /D NTDDI_VERSION=0x0A00000B /D _CRT_SECURE_NO_WARNINGS /D MWG_VERSION="6.0.0"` | `PreprocessorDefinitions` | +| `/D _WINDLL` | MSBuild adds it for every DLL project (Core, ActiveX) | +| `/diagnostics:column /FC /MP /nologo /errorReport:queue` | Logging and parallelism only; ignored by the parity script | + +Per configuration: + +| Configuration | Options | +|---|---| +| Debug | `/Od /MDd /RTC1 /JMC /D _DEBUG` (`/RTC1` is dropped when `MwgEnableAsan=true`, which adds `/fsanitize=address`) | +| Release | `/O2 /Oi /Gy /guard:cf /MD /D NDEBUG` | + +Per target: `MWG_CORE_BUILD` (Core), `_ATL_APARTMENT_THREADED` (ActiveX), +`MWG_ENABLE_RENDERER_TEST_HOOKS` (tests). + +Not defined by any project: `WIN32`, `_WINDOWS`, `_USRDLL`, `_CONSOLE`. The +CMake defaults `/DWIN32 /D_WINDOWS` are therefore removed, so the two builds +see the same preprocessor state. `/JMC` (Just My Code, a debugger aid) is an +MSBuild default for Debug that CMake does not add; CMake reproduces it. + +## Effective linker options + +| Option | Configurations | +|---|---| +| `/DEBUG` | Debug and Release (PDB for every binary) | +| `/DYNAMICBASE /NXCOMPAT /TLBID:1 /MANIFEST /manifest:embed /MANIFESTUAC:"level='asInvoker' uiAccess='false'"` | All | +| `/HIGHENTROPYVA` | x64 only (`MachineX64` also sets *large address aware*) | +| `/INCREMENTAL` | Debug | +| `/INCREMENTAL:NO /OPT:REF /OPT:ICF /guard:cf` | Release | +| `/SUBSYSTEM:WINDOWS /DLL` | Core, ActiveX | +| `/SUBSYSTEM:CONSOLE` | Tests, sample | +| `/DEF:MapWinGIS.ActiveX.def` and the compiled `.res` | ActiveX | +| `kernel32 user32 gdi32 winspool comdlg32 advapi32 shell32 ole32 oleaut32 uuid odbc32 odbccp32` | All (MSBuild default, same list as `CMAKE_CXX_STANDARD_LIBRARIES`) | +| `/[debug/]lib/*.lib` | All (`VcpkgAutoLink=true`) | + +### Real dependencies + +`VcpkgAutoLink` links every installed import library into every project and +hides what each target uses. From the sources and `dumpbin /dependents`: + +| Target | Libraries | +|---|---| +| Core | `GDAL::GDAL`, `GEOS::geos_c` (`GdalEnvironment.cpp` includes `geos_c.h` and calls `GEOSversion()`). PROJ is reached only through GDAL/OGR (`OSRSetPROJSearchPaths`). Windows: `kernel32` only. | +| ActiveX | Core, `ole32`, `oleaut32`, `uuid`, `user32`, `gdi32`, `advapi32` (registration), `opengl32` (through `#pragma comment(lib, ...)` in `OpenGlVectorLayerRenderer.cpp`), static ATL. No direct GDAL use. | +| Tests | Core, `GDAL::GDAL` (`gdal_priv.h`, `gdalalgorithm.h`, `vrtdataset.h`, `cpl_error.h`), `GTest::gtest` (not `gtest_main`: `Main.cpp` defines `main`), ATL headers (`atlbase.h`, `atlhost.h`), `ole32`, `oleaut32`, `uuid`, `user32`, `gdi32`, `opengl32`. | +| Sample | Core only. | + +`dumpbin` of the MSBuild tests shows `gmock.dll` instead of `gtest.dll`: the +auto-linked `gmock.lib` happens to export the GoogleTest symbols too. With +explicit linking the CMake build depends on `gtest.dll`; that is an expected +difference. + +## Generated files + +| File | Producer | Consumers | MSBuild location | +|---|---|---|---| +| `MapWinGIS.ActiveX_i.h` | MIDL (`/h`) | ActiveX sources, tests (`ActiveXActivationTests.cpp`) | `obj/MapWinGIS.ActiveX//` | +| `MapWinGIS.ActiveX_i.c` | MIDL (`/iid`) | Compiled into the OCX | same | +| `MapWinGIS.ActiveX_p.c`, `dlldata.c` | MIDL (`/proxy`, `/dlldata`) | None; MIDL writes no proxy for this IDL | — | +| `MapWinGIS.ActiveX.tlb` | MIDL (`/tlb`) | Embedded by `MapWinGIS.ActiveX.rc` | `bin//` | + +MIDL command line: `midl /W1 /nologo /char signed /env x64|win32 /h ... /iid ... +/proxy ... /tlb ... /target "NT60" MapWinGIS.ActiveX.idl`. The RC compiler +gets `/D UNICODE /D _UNICODE /l 0x0409` and the MIDL and output directories +as include paths. + +## Custom build steps + +- `MwgDeployGdalRuntimeData` (`Directory.Build.targets`, after `Build`, every + project unless `MwgDeployRuntimeData=false`): copies + `/share/gdal` → `/gdal-data` and `/share/proj` → + `/proj`, and warns when the GDAL data is missing. + `GdalEnvironment` looks for both folders beside the loaded module. +- `MwgInvalidateVcpkgInstallOnFeatureChange` / `MwgRecordVcpkgInstalledFeatures`: + MSBuild-only workaround for vcpkg's feature-blind stamp file. Not needed in + CMake: `vcpkg.cmake` runs `vcpkg install` on every configure with + `VCPKG_MANIFEST_FEATURES`. + +## Runtime deployment + +- `VcpkgApplocalDeps=true` copies the dependent vcpkg DLLs (GDAL, PROJ, GEOS, + curl, OpenSSL, SQLite, ...) beside each binary. +- `gdal-data/` and `proj/` (including `proj.db`) come from the custom step + above. +- All binaries share `bin//`. `ActiveXActivationTests` loads + `MapWinGIS.ocx` from the test executable's folder, and the C# host uses + registration-free COM through `app.manifest`, so the OCX must sit beside it. + +## Architecture-specific settings + +| Setting | x64 | Win32 | +|---|---|---| +| vcpkg triplet | `x64-windows-v145` | `x86-windows-v145` | +| vcpkg host triplet | `x64-windows-v145` | `x64-windows-v145` | +| MIDL `/env` | `x64` | `win32` | +| `/HIGHENTROPYVA` | yes | no | +| C# `PlatformTarget` / RID | `x64` / `win-x64` | `x86` / `win-x86` | + +## Hard-coded output paths + +These assume `/bin//`: + +- `samples/CSharp/ActiveXTestHost/Run-ActiveXTestHost.ps1` +- `tests/MapWinGIS.Core.Tests/Verify-ActiveXActivation.ps1` (OCX and test executable) +- `samples/CSharp/ActiveXTestHost/ActiveXTestHost.csproj` (`OutputPath`) +- `.github/workflows/build.yml` (test executable, Release artifact upload) + +## Prerequisites + +- Visual Studio 2026 with MSVC v145, Windows SDK 10.0.28000.0 and **C++ ATL + for latest v145 build tools for x64 and x86** (`atlmfc/lib/x64` and + `atlmfc/lib/x86`). +- CMake ≥ 3.28. VS 2026 and CLion both bundle CMake 4.3.1, which provides the + `Visual Studio 18 2026` generator and detects the v145 toolset. +- Ninja (bundled with VS and CLion) for single-config builds. +- MSBuild is only needed for the legacy build and parity comparisons. It is not + on `PATH` outside a Developer PowerShell; scripts resolve it through + `MWG_MSBUILD`, then `PATH`, then `vswhere`. + +## Decisions + +### D1. Output directory layout + +CMake writes to its build tree, never to `/bin`: runtime files to +`/bin`, import libraries to `/lib`, set once through +`CMAKE_RUNTIME_OUTPUT_DIRECTORY`, `CMAKE_LIBRARY_OUTPUT_DIRECTORY` and +`CMAKE_ARCHIVE_OUTPUT_DIRECTORY`. All targets and runtime dependencies of one +build share one runtime folder. Preset build directories are short +(`out/b/x64d`, `out/b/x64r`, `out/b/x86d`, `out/b/x86r`, `out/b/vs-x64`, +`out/b/vs-x86`) to stay clear of `MAX_PATH`. The scripts listed above gain an +optional `-BinDirectory` parameter in the phase that first needs it; their +default stays `bin//`. + +### D2. vcpkg install tree and triplets + +CMake uses its own install tree per platform, +`/vcpkg_installed/cmake-`, shared by the Debug and Release +presets and separate from the MSBuild tree `/vcpkg_installed/` +(different stamp files; a different feature set would prune packages the other +build needs). The binary cache makes the second install cheap. Presets pin +`VCPKG_TARGET_TRIPLET`, `VCPKG_HOST_TRIPLET=x64-windows-v145` and +`VCPKG_MANIFEST_FEATURES`; overlay triplets stay in `vcpkg-configuration.json`. +The vcpkg root is resolved before `project()` in the same order as +`bootstrap-vcpkg.ps1` (`MWG_VCPKG_ROOT`, sibling `../vcpkg`, `VCPKG_ROOT`), +unless `CMAKE_TOOLCHAIN_FILE` is already set. + +### D3. MIDL output ownership + +One custom target, `MapWinGIS.ActiveX.Interface`, owns the only MIDL +invocation and writes to `/src/MapWinGIS.ActiveX/midl`. A small library +target exposes that include directory and the generated `_i.c`. The OCX and +the tests both depend on that target, never on each other's intermediates. + +### D4. ActiveX sources compiled into the tests + +Option (a): one source-list variable for the six shared ActiveX sources, +compiled once into the OCX and once into the test executable with +`MWG_ENABLE_RENDERER_TEST_HOOKS`. This matches the MSBuild build and needs no +production code change. Making the hooks a runtime switch is out of scope. + +### D5. C# host project reference + +The `ProjectReference` from `ActiveXTestHost.csproj` to +`MapWinGIS.ActiveX.vcxproj` becomes conditional on +`'$(MwgCMakeBuild)' != 'true'`. The CMake custom targets pass +`-p:MwgCMakeBuild=true -p:BuildProjectReferences=false`, so `dotnet build` +never tries to build a `.vcxproj` or races CMake for the OCX. The imports in +`Directory.Build.props/.targets` are guarded by `Exists` and harmless under +`dotnet build`; the csproj already sets `MwgDeployRuntimeData=false`. + +### D6. Parity measurement + +Parity is measured with `build/scripts/Compare-CompileCommands.ps1`: + +- Compile options per translation unit and link options per binary, from the + detailed MSBuild log and from CMake's `compile_commands.json` plus a verbose + build log. Paths are normalised (``, ``, ``, + ``, ``), the vcpkg platform folder is collapsed and option + order is ignored. +- `dumpbin /dependents /exports /headers` summaries for selected binaries. +- Unique warning counts for both builds; CMake must not exceed MSBuild. + +The script runs against the MSBuild build alone (baseline) or with +`-CMakeBuildDir` for a comparison; MSBuild targets that the CMake build does +not have yet are skipped, so each phase can be measured on its own. + +```powershell +# Baseline: rebuilds MapWinGIS6.slnx with a detailed log under out\parity +pwsh -File build\scripts\Compare-CompileCommands.ps1 -Configuration Debug -Platform x64 ` + -Binaries 'MapWinGIS.Core.dll,MapWinGIS.ocx,MapWinGIS.Core.Tests.exe,translate.exe' + +# Comparison with a CMake Ninja build +cmake --build --preset x64-debug -- -v > out\parity\cmake-x64d.log +pwsh -File build\scripts\Compare-CompileCommands.ps1 -MSBuildLog out\parity\msbuild-Debug-x64.log ` + -CMakeBuildDir out\b\x64d -CMakeBuildLog out\parity\cmake-x64d.log -Binaries MapWinGIS.Core.dll +``` + +## Baseline + +| | Debug x64 | Release x64 | +|---|---|---| +| Translation units / linked binaries | 42 / 4 | 42 / 4 | +| Warnings | 0 | 0 | +| `MapWinGIS.Core.dll` dependents | `gdald`, `geos_c`, CRT | `gdal`, `geos_c`, CRT | +| `MapWinGIS.ocx` exports | 5 (`DllCanUnloadNow`, `DllGetClassObject`, `DllInstall`, `DllRegisterServer`, `DllUnregisterServer`) | 5 | +| DLL characteristics | Dynamic base, NX, high-entropy VA, large address aware | + Control Flow Guard | +| Native tests | 173 run, 170 passed, 3 skipped | — | + +The three skipped tests need `MWG_TEST_ACTIVEX_HOST`, `MWG_RUN_NETWORK_GDAL_TESTS` +or `MWG_RUN_RENDERING_BASELINE`. The Win32 baseline is collected in the phase +that first builds Win32 with CMake. From 90083a5c6b9c7f92eb7e69bf76cfedbebd7dce2d Mon Sep 17 00:00:00 2001 From: Paul Meems Date: Sat, 31 Oct 2026 11:42:00 +0100 Subject: [PATCH 2/3] Add CMake foundation and build MapWinGIS.Core with CMake Co-authored-by: Junie --- CMakeLists.txt | 72 +++++++++++++ CMakePresets.json | 124 ++++++++++++++++++++++ build/scripts/Compare-CompileCommands.ps1 | 41 ++++++- cmake/MwgCompilerOptions.cmake | 86 +++++++++++++++ cmake/MwgFlagOverrides.cmake | 19 ++++ cmake/MwgPlatform.cmake | 102 ++++++++++++++++++ cmake/MwgRuntimeDeploy.cmake | 50 +++++++++ cmake/MwgVcpkg.cmake | 113 ++++++++++++++++++++ docs/BUILDING.md | 61 +++++++++++ docs/CMAKE-MIGRATION.md | 30 +++++- src/MapWinGIS.Core/CMakeLists.txt | 77 ++++++++++++++ 11 files changed, 770 insertions(+), 5 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 cmake/MwgCompilerOptions.cmake create mode 100644 cmake/MwgFlagOverrides.cmake create mode 100644 cmake/MwgPlatform.cmake create mode 100644 cmake/MwgRuntimeDeploy.cmake create mode 100644 cmake/MwgVcpkg.cmake create mode 100644 src/MapWinGIS.Core/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c779136 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# MapWinGIS6 native build. Windows and MSVC v145 only. +# +# pwsh -File build\scripts\bootstrap-vcpkg.ps1 +# cmake --preset x64-debug +# cmake --build --preset x64-debug +# +# See docs/BUILDING.md and docs/CMAKE-MIGRATION.md. + +# 3.15: CMP0092 (no default /W3), 3.21: TARGET_RUNTIME_DLLS, +# 3.26: copy_directory_if_different, 3.28: tested minimum. +cmake_minimum_required(VERSION 3.28...4.3) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +# --- options needed before project() ----------------------------------------- +option(MWG_BUILD_TESTS "Build the GoogleTest suite (adds the 'tests' vcpkg feature)" ON) +option(MWG_BUILD_SAMPLES "Build the samples" ON) +set(MWG_WINDOWS_SDK "10.0.28000.0" CACHE STRING + "Windows SDK version (MSBuild: MwgWindowsSdk). 10.0 means the newest installed SDK") + +# --- configurations ---------------------------------------------------------- +# Debug and Release only, as in MSBuild. CLion offers RelWithDebInfo and +# MinSizeRel profiles by default; they are rejected instead of being built with +# flags that no other build uses. +if(CMAKE_GENERATOR MATCHES "^Visual Studio|Multi-Config$") + set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "Supported configurations" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Debug or Release" FORCE) + endif() + if(NOT CMAKE_BUILD_TYPE MATCHES "^(Debug|Release)$") + message(FATAL_ERROR + "CMAKE_BUILD_TYPE '${CMAKE_BUILD_TYPE}' is not supported; MapWinGIS6 builds Debug and Release only.\n" + "Use one of the presets in CMakePresets.json (CLion: disable the default profiles and enable the preset profiles).") + endif() + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release) +endif() + +# --- toolchain --------------------------------------------------------------- +include(MwgVcpkg) + +# Visual Studio generators select the Windows SDK from CMAKE_SYSTEM_VERSION; +# "10.0" lets them pick the newest one. Ninja uses the vcvars environment. +if(CMAKE_GENERATOR MATCHES "^Visual Studio" AND NOT DEFINED CMAKE_SYSTEM_VERSION) + set(CMAKE_SYSTEM_VERSION "${MWG_WINDOWS_SDK}" CACHE STRING "Windows SDK selected by the Visual Studio generator") +endif() + +set(CMAKE_USER_MAKE_RULES_OVERRIDE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/MwgFlagOverrides.cmake") + +project(MapWinGIS6 + VERSION 6.0.0 + DESCRIPTION "Open-source mapping engine behind MapWindow" + HOMEPAGE_URL "https://github.com/MapWindow/MapWinGIS6" + LANGUAGES C CXX) + +# --- output layout (docs/CMAKE-MIGRATION.md, D1) ----------------------------- +# One runtime folder per build tree, separate from MSBuild's bin\\. +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib") + +include(MwgPlatform) +include(MwgCompilerOptions) +include(MwgRuntimeDeploy) + +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + +mwg_add_runtime_data_target() + +add_subdirectory(src/MapWinGIS.Core) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..7f3771b --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,124 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 }, + "configurePresets": [ + { + "name": "windows-base", + "hidden": true, + "description": "Shared settings. The vcpkg toolchain is resolved by CMakeLists.txt (MWG_VCPKG_ROOT, ../vcpkg, VCPKG_ROOT).", + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "VCPKG_HOST_TRIPLET": "x64-windows-v145", + "VCPKG_MANIFEST_FEATURES": "tests" + } + }, + { + "name": "x64", + "hidden": true, + "description": "x64. 'architecture' only informs Visual Studio; in CLion assign a Visual Studio toolchain with architecture amd64.", + "architecture": { "value": "x64", "strategy": "external" }, + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-windows-v145", + "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed/cmake-x64" + } + }, + { + "name": "x86", + "hidden": true, + "description": "Win32. 'architecture' only informs Visual Studio; in CLion assign a Visual Studio toolchain with architecture x86 or amd64_x86.", + "architecture": { "value": "x86", "strategy": "external" }, + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x86-windows-v145", + "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed/cmake-Win32" + } + }, + { + "name": "debug", + "hidden": true, + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "release", + "hidden": true, + "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } + }, + { + "name": "ninja", + "hidden": true, + "description": "Single-config Ninja. Toolset and Windows SDK come from the vcvars environment (CLion toolchain, VS Developer PowerShell).", + "generator": "Ninja" + }, + { + "name": "vs2026", + "hidden": true, + "description": "Multi-config Visual Studio generator; pins toolset v145 and the Windows SDK (MWG_WINDOWS_SDK).", + "generator": "Visual Studio 18 2026", + "toolset": { "value": "v145", "strategy": "set" } + }, + { + "name": "x64-debug", + "displayName": "x64 Debug (Ninja)", + "inherits": [ "windows-base", "ninja", "x64", "debug" ], + "binaryDir": "${sourceDir}/out/b/x64d" + }, + { + "name": "x64-release", + "displayName": "x64 Release (Ninja)", + "inherits": [ "windows-base", "ninja", "x64", "release" ], + "binaryDir": "${sourceDir}/out/b/x64r" + }, + { + "name": "x86-debug", + "displayName": "Win32 Debug (Ninja)", + "inherits": [ "windows-base", "ninja", "x86", "debug" ], + "binaryDir": "${sourceDir}/out/b/x86d" + }, + { + "name": "x86-release", + "displayName": "Win32 Release (Ninja)", + "inherits": [ "windows-base", "ninja", "x86", "release" ], + "binaryDir": "${sourceDir}/out/b/x86r" + }, + { + "name": "vs2026-x64", + "displayName": "x64 (Visual Studio 18 2026)", + "inherits": [ "windows-base", "vs2026", "x64" ], + "architecture": { "value": "x64", "strategy": "set" }, + "binaryDir": "${sourceDir}/out/b/vs-x64" + }, + { + "name": "vs2026-x86", + "displayName": "Win32 (Visual Studio 18 2026)", + "inherits": [ "windows-base", "vs2026", "x86" ], + "architecture": { "value": "Win32", "strategy": "set" }, + "binaryDir": "${sourceDir}/out/b/vs-x86" + } + ], + "buildPresets": [ + { "name": "x64-debug", "configurePreset": "x64-debug" }, + { "name": "x64-release", "configurePreset": "x64-release" }, + { "name": "x86-debug", "configurePreset": "x86-debug" }, + { "name": "x86-release", "configurePreset": "x86-release" }, + { "name": "vs2026-x64-debug", "configurePreset": "vs2026-x64", "configuration": "Debug" }, + { "name": "vs2026-x64-release", "configurePreset": "vs2026-x64", "configuration": "Release" }, + { "name": "vs2026-x86-debug", "configurePreset": "vs2026-x86", "configuration": "Debug" }, + { "name": "vs2026-x86-release", "configurePreset": "vs2026-x86", "configuration": "Release" } + ], + "testPresets": [ + { + "name": "test-base", + "hidden": true, + "output": { "outputOnFailure": true }, + "filter": { "exclude": { "label": "interactive" } } + }, + { "name": "x64-debug", "inherits": "test-base", "configurePreset": "x64-debug" }, + { "name": "x64-release", "inherits": "test-base", "configurePreset": "x64-release" }, + { "name": "x86-debug", "inherits": "test-base", "configurePreset": "x86-debug" }, + { "name": "x86-release", "inherits": "test-base", "configurePreset": "x86-release" }, + { "name": "vs2026-x64-debug", "inherits": "test-base", "configurePreset": "vs2026-x64", "configuration": "Debug" }, + { "name": "vs2026-x64-release", "inherits": "test-base", "configurePreset": "vs2026-x64", "configuration": "Release" }, + { "name": "vs2026-x86-debug", "inherits": "test-base", "configurePreset": "vs2026-x86", "configuration": "Debug" }, + { "name": "vs2026-x86-release", "inherits": "test-base", "configurePreset": "vs2026-x86", "configuration": "Release" } + ] +} diff --git a/build/scripts/Compare-CompileCommands.ps1 b/build/scripts/Compare-CompileCommands.ps1 index 8fdc61f..006051a 100644 --- a/build/scripts/Compare-CompileCommands.ps1 +++ b/build/scripts/Compare-CompileCommands.ps1 @@ -73,6 +73,18 @@ $ignoredOptions = @( ) $ignoredOptionPrefixes = @('/Fo', '/Fd', '/Fe', '/Fp', '/ILK:', '/PDB:', '/IMPLIB:', '/OUT:', '/MP') +# Link differences that are deliberate (docs/CMAKE-MIGRATION.md). They are +# reported under 'explained' and do not count as lost options. +$explainedLinkDifferences = [ordered]@{ + '^/manifest(:embed|uac:.*)?$' = 'CMake''s vs_link wrapper generates and embeds the manifest with mt.exe; the embedded manifest is identical' + '^/tlbid:1$' = 'Only used when a type library resource is embedded' + '^/libpath:/' = 'vcpkg auto-link replaced by imported targets with full library paths' + '^lib:/.*\*\.lib$' = 'vcpkg auto-link replaced by imported targets with full library paths' + '^lib:/' = 'Library from an imported target (find_package)' + '^lib:odbc(cp)?32\.lib$' = 'MSBuild default library; unused, not in CMake''s default list' + '^/version:0\.0$' = 'CMake default image version; MSBuild leaves it at 0.0 too' +} + # MSBuild project names that differ from the CMake target names. $targetNames = @{ 'Translate' = 'MapWinGIS.Samples.Translate' } @@ -214,6 +226,8 @@ function ConvertTo-OptionSet([string[]] $arguments, [string] $baseDir, [hashtabl break } } + # Linker options are case-insensitive (/DEBUG and /debug are the same). + if ($isLink) { $arg = $arg.ToLowerInvariant() } $options.Add($arg) } @@ -458,11 +472,32 @@ if ($CMakeBuildDir) { $linkDiff = [ordered]@{} foreach ($binary in ($msbuildCommands.Links.Keys | Sort-Object)) { - if (-not $cmakeCommands.Links.ContainsKey($binary)) { continue } + if (-not $cmakeCommands.Links.ContainsKey($binary)) { + # A binary that CMake builds but that was up to date has no link + # line in the log; that is not parity. + $cmakeTarget = [System.IO.Path]::GetFileNameWithoutExtension($binary) + if ($CMakeBuildLog -and ($cmakeTargets -contains $cmakeTarget)) { + Write-Warning "$binary was not linked in $CMakeBuildLog; delete it and rebuild to compare its link options." + } + continue + } $diff = Compare-OptionSets $msbuildCommands.Links[$binary] $cmakeCommands.Links[$binary] + $explained = [ordered]@{} + $lost = @(); $added = @() + foreach ($pair in @(@{ Items = $diff.Lost; Kind = 'lost' }, @{ Items = $diff.Added; Kind = 'added' })) { + foreach ($option in $pair.Items) { + $reason = $null + foreach ($pattern in $explainedLinkDifferences.Keys) { + if ($option -match $pattern) { $reason = $explainedLinkDifferences[$pattern]; break } + } + if ($reason) { $explained["$($pair.Kind) $option"] = $reason } + elseif ($pair.Kind -eq 'lost') { $lost += $option } + else { $added += $option } + } + } if ($diff.Lost.Count -or $diff.Added.Count) { - $linkDiff[$binary] = $diff - $lostTotal += $diff.Lost.Count + $linkDiff[$binary] = [pscustomobject]@{ Lost = $lost; Added = $added; Explained = $explained } + $lostTotal += $lost.Count } } diff --git a/cmake/MwgCompilerOptions.cmake b/cmake/MwgCompilerOptions.cmake new file mode 100644 index 0000000..c54ef4d --- /dev/null +++ b/cmake/MwgCompilerOptions.cmake @@ -0,0 +1,86 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# mwg_apply_common_options() +# +# The compiler and linker settings of build/props/MapWinGIS.Cpp.props plus the +# MSBuild defaults they rely on, applied PRIVATE to one target so that nothing +# leaks to consumers of MapWinGIS.Core. The effective MSBuild options are listed +# in docs/CMAKE-MIGRATION.md; keep both in sync. + +option(MWG_ENABLE_ASAN "Compile with /fsanitize=address (MSBuild: MwgEnableAsan)" OFF) + +function(mwg_apply_common_options target) + get_target_property(type ${target} TYPE) + + set_target_properties(${target} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + # The MIDL-generated _i.c is C. + C_STANDARD 17 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL" + # PDBs in Debug and Release, as in MSBuild (CMake's default is Debug only). + MSVC_DEBUG_INFORMATION_FORMAT ProgramDatabase + # MSBuild passes the vcpkg include directory as a plain /I, so warnings + # in GDAL headers are reported at /W4. CMake would make imported + # include directories /external:I with /external:W0 and hide them. + NO_SYSTEM_FROM_IMPORTED ON + ) + + set(c_cxx "$") + set(debug "$>") + set(release "$>") + + target_compile_options(${target} PRIVATE + # WarningLevel, DisableSpecificWarnings. 4251 is safe: one toolset and + # one CRT for every binary of the product. + "$<${c_cxx}:/W4;/WX-;/wd4251;/external:W4>" + # AdditionalOptions + "$<${c_cxx}:/utf-8;/Zc:__cplusplus;/Zc:preprocessor;/Zc:throwingNew;/bigobj>" + # ConformanceMode, SDLCheck, ExceptionHandling and MSBuild defaults + "$<${c_cxx}:/permissive-;/sdl;/EHsc;/GS;/Gd;/Gm-;/fp:precise;/Zc:wchar_t;/Zc:forScope;/Zc:inline>" + "$<${debug}:/Od;/JMC>" + "$<${release}:/O2;/Oi;/Gy;/guard:cf>" + ) + if(MWG_ENABLE_ASAN) + target_compile_options(${target} PRIVATE "$<${c_cxx}:/fsanitize=address>") + else() + # BasicRuntimeChecks; incompatible with AddressSanitizer. + target_compile_options(${target} PRIVATE "$<${debug}:/RTC1>") + endif() + + target_compile_definitions(${target} PRIVATE + # CharacterSet=Unicode, also for the resource compiler. + UNICODE + _UNICODE + "$<${c_cxx}:WIN32_LEAN_AND_MEAN;NOMINMAX;_WIN32_WINNT=0x0A00;WINVER=0x0A00;NTDDI_VERSION=0x0A00000B;_CRT_SECURE_NO_WARNINGS>" + "$<${c_cxx}:MWG_VERSION=\"${PROJECT_VERSION}\">" + "$<${debug}:_DEBUG>" + "$<${release}:NDEBUG>" + ) + if(type STREQUAL "SHARED_LIBRARY") + # MSBuild defines _WINDLL for every DLL project. + target_compile_definitions(${target} PRIVATE "$<${c_cxx}:_WINDLL>") + endif() + + # CMake's defaults already add /machine, /debug /INCREMENTAL (Debug) and + # /INCREMENTAL:NO (Release). + target_link_options(${target} PRIVATE + /DYNAMICBASE + /NXCOMPAT + "$<$:/DEBUG;/OPT:REF;/OPT:ICF;/guard:cf>" + ) + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + target_link_options(${target} PRIVATE /HIGHENTROPYVA) + else() + # MSBuild Win32 defaults: keep frame pointers, no code analysis, and + # safe exception handler tables in Release. + target_compile_options(${target} PRIVATE "$<${c_cxx}:/Oy-;/analyze->") + target_link_options(${target} PRIVATE "$<$:/SAFESEH>") + endif() + if(type STREQUAL "SHARED_LIBRARY") + target_link_options(${target} PRIVATE /SUBSYSTEM:WINDOWS) + endif() +endfunction() diff --git a/cmake/MwgFlagOverrides.cmake b/cmake/MwgFlagOverrides.cmake new file mode 100644 index 0000000..1151d4c --- /dev/null +++ b/cmake/MwgFlagOverrides.cmake @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# CMAKE_USER_MAKE_RULES_OVERRIDE file: runs inside project(), after CMake's +# MSVC platform module and before the CMAKE__FLAGS* cache entries are +# created. +# +# CMake's MSVC defaults (/DWIN32 /D_WINDOWS /EHsc, /Ob0 /Od /RTC1, /O2 /Ob2 +# /DNDEBUG) do not match the MSBuild build, which defines neither WIN32 nor +# _WINDOWS. Every compile option comes from mwg_apply_common_options() +# instead (cmake/MwgCompilerOptions.cmake), so the targets carry exactly the +# flags documented in docs/CMAKE-MIGRATION.md and nothing leaks to consumers. +# Linker defaults (/machine, /debug /INCREMENTAL, /INCREMENTAL:NO) are kept. + +foreach(_mwg_lang IN ITEMS C CXX) + set(CMAKE_${_mwg_lang}_FLAGS_INIT "") + set(CMAKE_${_mwg_lang}_FLAGS_DEBUG_INIT "") + set(CMAKE_${_mwg_lang}_FLAGS_RELEASE_INIT "") +endforeach() +unset(_mwg_lang) diff --git a/cmake/MwgPlatform.cmake b/cmake/MwgPlatform.cmake new file mode 100644 index 0000000..00ccd5e --- /dev/null +++ b/cmake/MwgPlatform.cmake @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# Toolchain and platform validation, run after project(). +# +# Sets: +# MWG_PLATFORM MSBuild platform name: x64 or Win32 +# MWG_RUNTIME_DIR runtime output folder of this build (may contain $) + +if(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows") + message(FATAL_ERROR "MapWinGIS6 builds on Windows only.") +endif() + +foreach(_mwg_lang IN ITEMS C CXX) + if(NOT CMAKE_${_mwg_lang}_COMPILER_ID STREQUAL "MSVC") + message(FATAL_ERROR + "MapWinGIS6 needs the MSVC compiler (cl.exe); the ${_mwg_lang} compiler is " + "'${CMAKE_${_mwg_lang}_COMPILER_ID}' (${CMAKE_${_mwg_lang}_COMPILER}).\n" + "In CLion select a Visual Studio toolchain; from a shell, configure inside a " + "VS 2026 Developer PowerShell or vcvarsall.bat environment.") + endif() +endforeach() +unset(_mwg_lang) + +# v145 is MSVC 19.50 and later. The toolset name is not available under Ninja, +# so the compiler version is the check that works for every generator. +if(MSVC_VERSION LESS 1950) + message(FATAL_ERROR + "MapWinGIS6 needs the MSVC v145 toolset (Visual Studio 2026, MSVC 19.50 or later); " + "found MSVC ${CMAKE_CXX_COMPILER_VERSION} (${CMAKE_CXX_COMPILER}).") +endif() +if(CMAKE_GENERATOR_TOOLSET AND NOT CMAKE_GENERATOR_TOOLSET MATCHES "^v145") + message(FATAL_ERROR "MapWinGIS6 needs the v145 toolset; the generator uses '${CMAKE_GENERATOR_TOOLSET}'.") +endif() + +# --- architecture ------------------------------------------------------------- +# is an MSBuild concept: map the CMake architecture to x64 / Win32. +if(CMAKE_VS_PLATFORM_NAME) + set(MWG_PLATFORM "${CMAKE_VS_PLATFORM_NAME}") +elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(MWG_PLATFORM "x64") +else() + set(MWG_PLATFORM "Win32") +endif() + +if(MWG_PLATFORM STREQUAL "x64" AND CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_mwg_expected_triplet "x64-windows-v145") +elseif(MWG_PLATFORM STREQUAL "Win32" AND CMAKE_SIZEOF_VOID_P EQUAL 4) + set(_mwg_expected_triplet "x86-windows-v145") +else() + message(FATAL_ERROR "Unsupported target architecture '${MWG_PLATFORM}' (pointer size ${CMAKE_SIZEOF_VOID_P}).") +endif() + +# A mismatch means the CLion toolchain or vcvars environment targets another +# architecture than the preset; linking would fail much later with LNK1112. +if(NOT VCPKG_TARGET_TRIPLET STREQUAL _mwg_expected_triplet) + message(FATAL_ERROR + "VCPKG_TARGET_TRIPLET is '${VCPKG_TARGET_TRIPLET}', but the compiler targets ${MWG_PLATFORM} " + "(expected '${_mwg_expected_triplet}').\n" + "In CLion, assign this profile a Visual Studio toolchain with the matching architecture " + "(amd64 for x64 presets, x86 or amd64_x86 for x86 presets), then Reset Cache and Reload.") +endif() +unset(_mwg_expected_triplet) + +# --- Windows SDK -------------------------------------------------------------- +# Visual Studio generators pin the SDK through CMAKE_SYSTEM_VERSION (set from +# MWG_WINDOWS_SDK before project()). Ninja takes the SDK from the vcvars +# environment and cannot pin it, so a mismatch is only reported. +if(NOT MWG_WINDOWS_SDK STREQUAL "10.0") + if(CMAKE_GENERATOR MATCHES "^Visual Studio") + if(NOT CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION STREQUAL MWG_WINDOWS_SDK) + message(FATAL_ERROR + "Windows SDK ${MWG_WINDOWS_SDK} is not installed (Visual Studio selected " + "'${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}'). Install it, or configure with " + "-DMWG_WINDOWS_SDK=10.0 to use the newest installed SDK.") + endif() + else() + set(_mwg_sdk "$ENV{WindowsSDKVersion}") + string(REGEX REPLACE "[\\/]+$" "" _mwg_sdk "${_mwg_sdk}") + if(_mwg_sdk STREQUAL "" AND CMAKE_RC_COMPILER MATCHES "Windows Kits/10/bin/([0-9.]+)/") + set(_mwg_sdk "${CMAKE_MATCH_1}") + endif() + if(NOT _mwg_sdk STREQUAL MWG_WINDOWS_SDK) + message(WARNING + "The build environment uses Windows SDK '${_mwg_sdk}', not ${MWG_WINDOWS_SDK}. " + "Ninja cannot pin the SDK; it comes from vcvarsall.bat. Set the Version field of the " + "CLion Visual Studio toolchain to ${MWG_WINDOWS_SDK} (equivalent to " + "'vcvarsall.bat ${MWG_WINDOWS_SDK}'), or set -DMWG_WINDOWS_SDK=10.0 to accept the newest SDK.") + endif() + unset(_mwg_sdk) + endif() +endif() + +# --- runtime folder ----------------------------------------------------------- +get_property(_mwg_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(_mwg_multi_config) + set(MWG_RUNTIME_DIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$") +else() + set(MWG_RUNTIME_DIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") +endif() +unset(_mwg_multi_config) + +message(STATUS "MapWinGIS6: ${MWG_PLATFORM}, MSVC ${CMAKE_CXX_COMPILER_VERSION}, triplet ${VCPKG_TARGET_TRIPLET}") diff --git a/cmake/MwgRuntimeDeploy.cmake b/cmake/MwgRuntimeDeploy.cmake new file mode 100644 index 0000000..10bfbdf --- /dev/null +++ b/cmake/MwgRuntimeDeploy.cmake @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# Runtime data deployment (MSBuild: MwgDeployGdalRuntimeData). +# +# vcpkg installs the GDAL and PROJ data under share/; GdalEnvironment looks for +# "gdal-data" and "proj" beside the loaded module. The target +# MapWinGIS.RuntimeData copies both into the runtime folder. A stamp file in +# that folder keeps the copy from running on every build: it reruns only when +# a data file changes or the runtime folder was cleaned. + +option(MWG_DEPLOY_RUNTIME_DATA "Copy gdal-data and proj into the runtime folder (MSBuild: MwgDeployRuntimeData)" ON) + +function(mwg_add_runtime_data_target) + set(share_dir "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share") + set(gdal_dir "${share_dir}/gdal") + set(proj_dir "${share_dir}/proj") + + if(NOT MWG_DEPLOY_RUNTIME_DATA) + add_custom_target(MapWinGIS.RuntimeData) + return() + endif() + + if(NOT EXISTS "${gdal_dir}") + message(WARNING + "GDAL runtime data was not found in ${gdal_dir}. " + "Run build\\scripts\\bootstrap-vcpkg.ps1 and reconfigure.") + add_custom_target(MapWinGIS.RuntimeData) + return() + endif() + + file(GLOB_RECURSE data_files CONFIGURE_DEPENDS "${gdal_dir}/*" "${proj_dir}/*") + + set(stamp "${MWG_RUNTIME_DIR}/.mwg-runtime-data.stamp") + set(commands + COMMAND "${CMAKE_COMMAND}" -E copy_directory_if_different "${gdal_dir}" "${MWG_RUNTIME_DIR}/gdal-data") + if(EXISTS "${proj_dir}") + list(APPEND commands + COMMAND "${CMAKE_COMMAND}" -E copy_directory_if_different "${proj_dir}" "${MWG_RUNTIME_DIR}/proj") + endif() + + add_custom_command( + OUTPUT "${stamp}" + ${commands} + COMMAND "${CMAKE_COMMAND}" -E touch "${stamp}" + DEPENDS ${data_files} + COMMENT "Deploying GDAL and PROJ runtime data" + VERBATIM + ) + add_custom_target(MapWinGIS.RuntimeData DEPENDS "${stamp}") +endfunction() diff --git a/cmake/MwgVcpkg.cmake b/cmake/MwgVcpkg.cmake new file mode 100644 index 0000000..30c3e50 --- /dev/null +++ b/cmake/MwgVcpkg.cmake @@ -0,0 +1,113 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# vcpkg integration. Included by the root CMakeLists.txt BEFORE project(), so +# that the toolchain file and the vcpkg variables are in place when project() +# runs vcpkg's manifest install. +# +# Values from a preset or the command line always win; this module only fills +# in what is missing. + +# --- vcpkg root --------------------------------------------------------------- +# Same order as build/scripts/bootstrap-vcpkg.ps1 and Directory.Build.props: +# 1. MWG_VCPKG_ROOT (cache variable or environment variable) +# 2. /../vcpkg +# 3. VCPKG_ROOT +function(_mwg_is_vcpkg_root out_var dir) + if(EXISTS "${dir}/.vcpkg-root" AND EXISTS "${dir}/scripts/buildsystems/vcpkg.cmake") + set(${out_var} TRUE PARENT_SCOPE) + else() + set(${out_var} FALSE PARENT_SCOPE) + endif() +endfunction() + +function(_mwg_resolve_vcpkg_root out_var) + # An explicit MWG_VCPKG_ROOT that is not a vcpkg root is a mistake, not a hint. + set(explicit "${MWG_VCPKG_ROOT}") + if(explicit STREQUAL "" AND DEFINED ENV{MWG_VCPKG_ROOT}) + set(explicit "$ENV{MWG_VCPKG_ROOT}") + endif() + if(NOT explicit STREQUAL "") + file(TO_CMAKE_PATH "${explicit}" explicit) + _mwg_is_vcpkg_root(ok "${explicit}") + if(NOT ok) + message(FATAL_ERROR + "MWG_VCPKG_ROOT points to '${explicit}', which is not a vcpkg root.\n" + "Run 'pwsh -File build\\scripts\\bootstrap-vcpkg.ps1' or fix MWG_VCPKG_ROOT.") + endif() + set(${out_var} "${explicit}" PARENT_SCOPE) + return() + endif() + + cmake_path(GET CMAKE_CURRENT_SOURCE_DIR PARENT_PATH repo_parent) + set(candidates "${repo_parent}/vcpkg") + if(DEFINED ENV{VCPKG_ROOT} AND NOT "$ENV{VCPKG_ROOT}" STREQUAL "") + file(TO_CMAKE_PATH "$ENV{VCPKG_ROOT}" env_root) + list(APPEND candidates "${env_root}") + endif() + + foreach(candidate IN LISTS candidates) + _mwg_is_vcpkg_root(ok "${candidate}") + if(ok) + set(${out_var} "${candidate}" PARENT_SCOPE) + return() + endif() + endforeach() + + message(FATAL_ERROR + "No vcpkg installation found (looked at MWG_VCPKG_ROOT, ${repo_parent}/vcpkg and VCPKG_ROOT).\n" + "Run 'pwsh -File build\\scripts\\bootstrap-vcpkg.ps1' once, or pass -DCMAKE_TOOLCHAIN_FILE=/scripts/buildsystems/vcpkg.cmake.") +endfunction() + +if(NOT DEFINED CMAKE_TOOLCHAIN_FILE) + _mwg_resolve_vcpkg_root(_mwg_vcpkg_root) + set(CMAKE_TOOLCHAIN_FILE "${_mwg_vcpkg_root}/scripts/buildsystems/vcpkg.cmake" + CACHE FILEPATH "vcpkg toolchain file, resolved from MWG_VCPKG_ROOT, ../vcpkg or VCPKG_ROOT") + message(STATUS "vcpkg root: ${_mwg_vcpkg_root}") +endif() + +# --- triplets ----------------------------------------------------------------- +# The presets pin the triplets. Without a preset, derive the target triplet from +# the generator platform (Visual Studio) or the vcvars environment (Ninja). +# Without an explicit triplet vcpkg silently picks x64-windows and builds GDAL a +# second time with a different toolset. MwgPlatform.cmake checks after +# project() that the triplet matches the compiler. +if(NOT DEFINED VCPKG_TARGET_TRIPLET) + if(CMAKE_GENERATOR_PLATFORM MATCHES "^(Win32|x86)" OR "$ENV{VSCMD_ARG_TGT_ARCH}" STREQUAL "x86") + set(_mwg_default_triplet "x86-windows-v145") + else() + set(_mwg_default_triplet "x64-windows-v145") + endif() + set(VCPKG_TARGET_TRIPLET "${_mwg_default_triplet}" CACHE STRING "vcpkg target triplet") +endif() + +if(NOT DEFINED VCPKG_HOST_TRIPLET) + set(VCPKG_HOST_TRIPLET "x64-windows-v145" CACHE STRING "vcpkg host triplet") +endif() + +# --- install tree ------------------------------------------------------------- +# One CMake install tree per platform, shared by Debug and Release and separate +# from the MSBuild tree (vcpkg_installed/), see docs/CMAKE-MIGRATION.md (D2). +if(NOT DEFINED VCPKG_INSTALLED_DIR) + if(VCPKG_TARGET_TRIPLET MATCHES "^x86-") + set(_mwg_vcpkg_platform "Win32") + else() + set(_mwg_vcpkg_platform "x64") + endif() + set(VCPKG_INSTALLED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg_installed/cmake-${_mwg_vcpkg_platform}" + CACHE PATH "vcpkg install tree used by the CMake build") +endif() + +# --- manifest features -------------------------------------------------------- +# vcpkg.cmake runs 'vcpkg install' on every configure with these features, so a +# change takes effect on the next reconfigure (CLion: Reset Cache and Reload +# when a cached value is stale). +if(NOT DEFINED VCPKG_MANIFEST_FEATURES) + set(_mwg_features "") + if(MWG_BUILD_TESTS) + list(APPEND _mwg_features "tests") + endif() + set(VCPKG_MANIFEST_FEATURES "${_mwg_features}" CACHE STRING "vcpkg manifest features (tests, full-drivers)") +elseif(MWG_BUILD_TESTS AND NOT "tests" IN_LIST VCPKG_MANIFEST_FEATURES) + message(STATUS "MWG_BUILD_TESTS is ON: adding the 'tests' vcpkg manifest feature") + list(APPEND VCPKG_MANIFEST_FEATURES "tests") +endif() diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 53a2fa9..d8315d0 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -196,6 +196,67 @@ the MSBuild solution above stays the supported build. The audit of the current MSBuild settings and the decisions the CMake build follows are in [CMAKE-MIGRATION.md](CMAKE-MIGRATION.md). +### Building with CMake + +The CMake build currently covers `MapWinGIS.Core.dll`; the ActiveX control, +tests and samples follow. It needs CMake 3.28 or later and Ninja; both ship +with Visual Studio 2026 and CLion. Run the bootstrap script first, then +configure from a VS 2026 Developer PowerShell (or any shell after +`vcvarsall.bat x64`): + +```powershell +pwsh -File build\scripts\bootstrap-vcpkg.ps1 +cmake --preset x64-debug +cmake --build --preset x64-debug +``` + +| Preset | Generator | Build directory | +|---|---|---| +| `x64-debug`, `x64-release` | Ninja, x64 | `out\b\x64d`, `out\b\x64r` | +| `x86-debug`, `x86-release` | Ninja, Win32 (vcvars `x86` or `amd64_x86`) | `out\b\x86d`, `out\b\x86r` | +| `vs2026-x64`, `vs2026-x86` | Visual Studio 18 2026, `-T v145` (build presets `vs2026-x64-debug`, ...) | `out\b\vs-x64`, `out\b\vs-x86` | + +- Output goes to `\bin` (`bin\` for the Visual Studio + presets) and import libraries to `lib`, never to the MSBuild `bin\` folder, so + both builds can be used side by side. +- The vcpkg root is found in the same order as above. CMake uses its own install + tree, `vcpkg_installed\cmake-`, shared by Debug and Release; the + binary cache makes it cheap. +- Manifest features come from `VCPKG_MANIFEST_FEATURES` (presets: `tests`). A + change takes effect on the next configure; in CLion use + **Tools | CMake | Reset Cache and Reload Project** when a cached value is stale. +- Only `Debug` and `Release` are supported. Other `CMAKE_BUILD_TYPE` values, + such as CLion's default `RelWithDebInfo`, are rejected. +- Ninja takes the toolset and Windows SDK from the vcvars environment and + cannot pin them; configure warns when the SDK differs from `MWG_WINDOWS_SDK`. + The Visual Studio presets pin both. +- The presets' `architecture` only informs Visual Studio. In CLion, give each + preset profile a Visual Studio toolchain with the matching architecture + (`amd64` for x64, `x86` or `amd64_x86` for x86); a mismatch stops configure + with an explanation. + +| CMake option | Default | MSBuild equivalent | +|---|---|---| +| `MWG_WINDOWS_SDK` | `10.0.28000.0` | `MwgWindowsSdk` (`10.0` = newest) | +| `MWG_VCPKG_ROOT` | resolved | `MwgVcpkgRoot` | +| `VCPKG_MANIFEST_FEATURES` | `tests` | `MwgVcpkgFeatures` | +| `MWG_ENABLE_ASAN` | `OFF` | `MwgEnableAsan` | +| `MWG_DEPLOY_RUNTIME_DATA` | `ON` | `MwgDeployRuntimeData` | +| `MWG_BUILD_TESTS` | `ON` | — (adds the `tests` feature) | +| `MWG_BUILD_SAMPLES` | `ON` | — | + +Machine-specific settings belong in an uncommitted `CMakeUserPresets.json` +that inherits from the presets, for example: + +```json +{ + "version": 6, + "configurePresets": [ + { "name": "my-x64-debug", "inherits": "x64-debug", "cacheVariables": { "MWG_WINDOWS_SDK": "10.0" } } + ] +} +``` + To measure flag and binary parity, `build\scripts\Compare-CompileCommands.ps1` records a detailed MSBuild log and writes a normalised report to `out\parity\`. It finds MSBuild through `MWG_MSBUILD`, `PATH` or `vswhere`, so it also works diff --git a/docs/CMAKE-MIGRATION.md b/docs/CMAKE-MIGRATION.md index 89e6eb6..7cf1e68 100644 --- a/docs/CMAKE-MIGRATION.md +++ b/docs/CMAKE-MIGRATION.md @@ -300,5 +300,31 @@ pwsh -File build\scripts\Compare-CompileCommands.ps1 -MSBuildLog out\parity\msbu | Native tests | 173 run, 170 passed, 3 skipped | — | The three skipped tests need `MWG_TEST_ACTIVEX_HOST`, `MWG_RUN_NETWORK_GDAL_TESTS` -or `MWG_RUN_RENDERING_BASELINE`. The Win32 baseline is collected in the phase -that first builds Win32 with CMake. +or `MWG_RUN_RENDERING_BASELINE`. The Win32 baseline was collected in Phase 1 +(Debug and Release, 42 TUs / 4 links, 0 warnings). + +## Parity status + +### MapWinGIS.Core (Phase 1) + +Measured for Debug and Release, x64 and Win32 (Ninja presets): + +- Compile options: none lost; 0 warnings in both builds. +- Link options: none lost. The remaining differences are explained by the + parity script (`diff.links..Explained`): + - `/MANIFEST /manifest:embed /MANIFESTUAC:...`: CMake's `vs_link_dll` wrapper + generates and embeds the manifest with `mt.exe`; the embedded manifest + resource is byte-identical to the MSBuild one. + - `/TLBID:1`: only relevant when a type library resource is embedded (OCX). + - vcpkg `/LIBPATH` and `*.lib` auto-link: replaced by `GDAL::GDAL` and + `GEOS::geos_c` with full library paths. + - `odbc32.lib odbccp32.lib`: MSBuild defaults, unused, not in CMake's default + `CMAKE_CXX_STANDARD_LIBRARIES`. + - `/version:0.0`: added by CMake; the image version is 0.0 in both builds. +- Added compile options, from the imported targets: `/I/include/geos` + (GEOS's interface include directory) and, in Debug, `/DGDAL_DEBUG` + (`GDAL::GDAL` interface definition for the debug library). +- Win32 needs `/Oy- /analyze-` and, in Release, `/SAFESEH`; MSBuild adds them + by default for that platform. +- `dumpbin`: identical dependents, exports (260), subsystem and DLL + characteristics. diff --git a/src/MapWinGIS.Core/CMakeLists.txt b/src/MapWinGIS.Core/CMakeLists.txt new file mode 100644 index 0000000..9f7bcc3 --- /dev/null +++ b/src/MapWinGIS.Core/CMakeLists.txt @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# MapWinGIS.Core.dll: the mapping engine. No COM, ATL or MFC. + +find_package(GDAL CONFIG REQUIRED) +find_package(GEOS CONFIG REQUIRED) + +set(MWG_CORE_PUBLIC_HEADERS + include/mapwingis/core/Api.h + include/mapwingis/core/Enums.h + include/mapwingis/core/Error.h + include/mapwingis/core/Extent.h + include/mapwingis/gdal/GdalAlgorithmRunner.h + include/mapwingis/gdal/GdalConfig.h + include/mapwingis/gdal/GdalDiagnostics.h + include/mapwingis/gdal/GdalEnvironment.h + include/mapwingis/gdal/GdalModern.h + include/mapwingis/gdal/GdalOperationResult.h + include/mapwingis/gdal/GdalProgress.h + include/mapwingis/gdal/GdalTypedOptions.h + include/mapwingis/gdal/GdalUtils.h + include/mapwingis/rendering/VectorRenderScene.h + include/mapwingis/vector/FieldInfo.h + include/mapwingis/vector/VectorDataset.h +) + +set(MWG_CORE_PRIVATE_HEADERS + src/internal/GdalExecution.h + src/internal/GdalProgressAdapterAccess.h + src/internal/Utf8.h + src/internal/VectorLayerAccess.h +) + +set(MWG_CORE_SOURCES + src/gdal/GdalAlgorithmRunner.cpp + src/gdal/GdalEnvironment.cpp + src/gdal/GdalModern.cpp + src/gdal/GdalOperationResult.cpp + src/gdal/GdalRuntime.cpp + src/gdal/GdalTypedOptions.cpp + src/gdal/GdalUtils.cpp + src/rendering/VectorRenderScene.cpp + src/vector/VectorDataset.cpp +) + +add_library(MapWinGIS.Core SHARED) +add_library(MapWinGIS::Core ALIAS MapWinGIS.Core) + +target_sources(MapWinGIS.Core PRIVATE + ${MWG_CORE_PUBLIC_HEADERS} + ${MWG_CORE_PRIVATE_HEADERS} + ${MWG_CORE_SOURCES} +) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES + ${MWG_CORE_PUBLIC_HEADERS} + ${MWG_CORE_PRIVATE_HEADERS} + ${MWG_CORE_SOURCES} +) + +target_include_directories(MapWinGIS.Core + PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" +) + +# MWG_CORE_BUILD selects __declspec(dllexport) in mapwingis/core/Api.h. Using it +# as the DEFINE_SYMBOL also replaces CMake's default MapWinGIS_Core_EXPORTS. +set_target_properties(MapWinGIS.Core PROPERTIES DEFINE_SYMBOL MWG_CORE_BUILD) + +# No public header includes GDAL or GEOS, so both stay private. GEOS is used +# directly: GdalEnvironment::GeosVersion() calls GEOSversion(). +target_link_libraries(MapWinGIS.Core PRIVATE GDAL::GDAL GEOS::geos_c) + +mwg_apply_common_options(MapWinGIS.Core) + +set_target_properties(MapWinGIS.Core PROPERTIES FOLDER "src") + +add_dependencies(MapWinGIS.Core MapWinGIS.RuntimeData) From 08c943ad35de6650edec1d30c53745ec31445bf6 Mon Sep 17 00:00:00 2001 From: Paul Meems Date: Thu, 12 Nov 2026 14:08:37 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=94=A7=20build(cmake):=20resolve=20CM?= =?UTF-8?q?ake=20review=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Align Core CMake sources and headers with MSBuild - Fix ASan and runtime data deployment - Clean CMake docs and add CI coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 40 +++++++++++++++++++++++++++ CMakeLists.txt | 1 - cmake/MwgCompilerOptions.cmake | 1 + cmake/MwgRuntimeDeploy.cmake | 7 ++++- docs/BUILDING.md | 13 --------- src/MapWinGIS.Core/CMakeLists.txt | 46 +++++++++++++++++++++++++++++++ 6 files changed, 93 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b97a6e..85a3d37 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,46 @@ concurrency: cancel-in-progress: true jobs: + cmake: + runs-on: windows-2025 + timeout-minutes: 180 + + env: + VCPKG_BINARY_SOURCES: 'clear;files,${{ github.workspace }}\vcpkg-binary-cache,readwrite' + MWG_WINDOWS_SDK: '10.0' + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Record runner image version + id: runner_image + shell: pwsh + run: Add-Content -Path $env:GITHUB_OUTPUT -Value "image_version=$env:ImageVersion" + + - name: Cache vcpkg binary packages + uses: actions/cache@v6 + with: + path: vcpkg-binary-cache + key: vcpkg-binary-${{ runner.os }}-x64-${{ steps.runner_image.outputs.image_version }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'build/triplets/*.cmake', 'Directory.Build.props') }} + restore-keys: | + vcpkg-binary-${{ runner.os }}-x64- + + - name: Bootstrap vcpkg + shell: pwsh + run: ./build/scripts/bootstrap-vcpkg.ps1 + + - name: Build CMake preset + shell: pwsh + run: | + $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + $vsPath = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (-not $vsPath) { throw 'Visual Studio C++ tools were not found.' } + $vcvars = Join-Path $vsPath 'VC\Auxiliary\Build\vcvars64.bat' + $commands = 'call "{0}" && cmake --preset x64-debug -DMWG_WINDOWS_SDK={1} && cmake --build --preset x64-debug' -f $vcvars,$env:MWG_WINDOWS_SDK + & $env:ComSpec /d /s /c $commands + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + build: runs-on: windows-2025 timeout-minutes: 180 diff --git a/CMakeLists.txt b/CMakeLists.txt index c779136..8a26e1a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,6 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") # --- options needed before project() ----------------------------------------- option(MWG_BUILD_TESTS "Build the GoogleTest suite (adds the 'tests' vcpkg feature)" ON) -option(MWG_BUILD_SAMPLES "Build the samples" ON) set(MWG_WINDOWS_SDK "10.0.28000.0" CACHE STRING "Windows SDK version (MSBuild: MwgWindowsSdk). 10.0 means the newest installed SDK") diff --git a/cmake/MwgCompilerOptions.cmake b/cmake/MwgCompilerOptions.cmake index c54ef4d..e403fc0 100644 --- a/cmake/MwgCompilerOptions.cmake +++ b/cmake/MwgCompilerOptions.cmake @@ -46,6 +46,7 @@ function(mwg_apply_common_options target) ) if(MWG_ENABLE_ASAN) target_compile_options(${target} PRIVATE "$<${c_cxx}:/fsanitize=address>") + target_link_options(${target} PRIVATE /INCREMENTAL:NO) else() # BasicRuntimeChecks; incompatible with AddressSanitizer. target_compile_options(${target} PRIVATE "$<${debug}:/RTC1>") diff --git a/cmake/MwgRuntimeDeploy.cmake b/cmake/MwgRuntimeDeploy.cmake index 10bfbdf..23dc3eb 100644 --- a/cmake/MwgRuntimeDeploy.cmake +++ b/cmake/MwgRuntimeDeploy.cmake @@ -29,9 +29,14 @@ function(mwg_add_runtime_data_target) endif() file(GLOB_RECURSE data_files CONFIGURE_DEPENDS "${gdal_dir}/*" "${proj_dir}/*") + list(SORT data_files) + + set(input_manifest "${MWG_RUNTIME_DIR}/.mwg-runtime-data.inputs") + file(GENERATE OUTPUT "${input_manifest}" CONTENT "${data_files}") set(stamp "${MWG_RUNTIME_DIR}/.mwg-runtime-data.stamp") set(commands + COMMAND "${CMAKE_COMMAND}" -E rm -rf "${MWG_RUNTIME_DIR}/gdal-data" "${MWG_RUNTIME_DIR}/proj" COMMAND "${CMAKE_COMMAND}" -E copy_directory_if_different "${gdal_dir}" "${MWG_RUNTIME_DIR}/gdal-data") if(EXISTS "${proj_dir}") list(APPEND commands @@ -42,7 +47,7 @@ function(mwg_add_runtime_data_target) OUTPUT "${stamp}" ${commands} COMMAND "${CMAKE_COMMAND}" -E touch "${stamp}" - DEPENDS ${data_files} + DEPENDS ${data_files} "${input_manifest}" COMMENT "Deploying GDAL and PROJ runtime data" VERBATIM ) diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 80772fc..57bd5c8 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -233,13 +233,6 @@ records a detailed MSBuild log and writes a normalised report to `out\parity\`. It finds MSBuild through `MWG_MSBUILD`, `PATH` or `vswhere`, so it also works when Visual Studio is not on `PATH` or not installed on `C:`. -## CMake migration - -CMake is being introduced as the primary native build. Until it is complete, -the MSBuild solution above stays the supported build. The audit of the current -MSBuild settings and the decisions the CMake build follows are in -[CMAKE-MIGRATION.md](CMAKE-MIGRATION.md). - ### Building with CMake The CMake build currently covers `MapWinGIS.Core.dll`; the ActiveX control, @@ -287,7 +280,6 @@ cmake --build --preset x64-debug | `MWG_ENABLE_ASAN` | `OFF` | `MwgEnableAsan` | | `MWG_DEPLOY_RUNTIME_DATA` | `ON` | `MwgDeployRuntimeData` | | `MWG_BUILD_TESTS` | `ON` | — (adds the `tests` feature) | -| `MWG_BUILD_SAMPLES` | `ON` | — | Machine-specific settings belong in an uncommitted `CMakeUserPresets.json` that inherits from the presets, for example: @@ -301,11 +293,6 @@ that inherits from the presets, for example: } ``` -To measure flag and binary parity, `build\scripts\Compare-CompileCommands.ps1` -records a detailed MSBuild log and writes a normalised report to `out\parity\`. -It finds MSBuild through `MWG_MSBUILD`, `PATH` or `vswhere`, so it also works -when Visual Studio is not on `PATH` or not installed on `C:`. - ## Troubleshooting **`Invalid triplet: x64-windows-v145`** diff --git a/src/MapWinGIS.Core/CMakeLists.txt b/src/MapWinGIS.Core/CMakeLists.txt index 9f7bcc3..c5e1a3c 100644 --- a/src/MapWinGIS.Core/CMakeLists.txt +++ b/src/MapWinGIS.Core/CMakeLists.txt @@ -19,6 +19,10 @@ set(MWG_CORE_PUBLIC_HEADERS include/mapwingis/gdal/GdalProgress.h include/mapwingis/gdal/GdalTypedOptions.h include/mapwingis/gdal/GdalUtils.h + include/mapwingis/map/Map.h + include/mapwingis/map/MapView.h + include/mapwingis/map/Viewport.h + include/mapwingis/rendering/MapRenderScene.h include/mapwingis/rendering/VectorRenderScene.h include/mapwingis/vector/FieldInfo.h include/mapwingis/vector/VectorDataset.h @@ -29,6 +33,7 @@ set(MWG_CORE_PRIVATE_HEADERS src/internal/GdalProgressAdapterAccess.h src/internal/Utf8.h src/internal/VectorLayerAccess.h + src/internal/VectorRenderSceneBuilder.h ) set(MWG_CORE_SOURCES @@ -39,10 +44,51 @@ set(MWG_CORE_SOURCES src/gdal/GdalRuntime.cpp src/gdal/GdalTypedOptions.cpp src/gdal/GdalUtils.cpp + src/map/Map.cpp + src/map/MapView.cpp + src/map/Viewport.cpp + src/rendering/MapRenderScene.cpp src/rendering/VectorRenderScene.cpp src/vector/VectorDataset.cpp ) +# Keep CMake's target sources and headers aligned with the MSBuild project. +set(_mwg_core_vcxproj "${CMAKE_CURRENT_SOURCE_DIR}/MapWinGIS.Core.vcxproj") +file(STRINGS "${_mwg_core_vcxproj}" _mwg_core_vcxproj_items + REGEX "