Publish-DotnetApplication.psm1
|
# # Publish-DotnetApplication # # The dotnet CLI only counterpart of Publish-Application. Same parameters, same lomtec/nuaktiv.json # configuration files, same outputs - but every build goes through `dotnet build` / `dotnet publish`, # so it needs the .NET SDK and nothing from Visual Studio. # # How a run goes: # 1. plan every config file next to a csproj becomes a plan (publish entry or nuget package) # 2. validate all plans are checked BEFORE the first build: runtime looks like a RID, no two # publishes writing to the same folder. Every problem is reported at once, nothing # is built. A nuget config without its nuspec is only a warning: the project is # still built, no package is produced for it. # 3. build nuget projects are built with ONE solution build per configuration through a # generated solution filter (.slnf) that lists only those projects - anything else in # the solution (sqlproj, tests, ...) is never loaded. Falls back to per project builds # when a project is not one the dotnet CLI can build. # 4. publish every publish entry runs `dotnet publish` with its own runtime and properties # 5. pack `dotnet pack --no-build` for every nuget plan, in parallel, driven by the nuspec # 6. cleanup pdb / config leftovers in the publish output, the .slnf files, the build servers # # What is different to Publish-Application: # - "Build.Command": "msbuild" in a config is accepted but ignored (with a warning). There is no # MSBuild.exe path in here. # # A note on the word "MSBuild" in this file: `dotnet build`, `dotnet publish` and `dotnet pack` ARE # MSBuild - the dotnet CLI hosts the MSBuild engine (MSBuild.dll running on .NET). That engine is what # -m (build nodes), node reuse, the binary logs (.binlog) and the build server shutdown refer to. # What this module never uses is MSBuild.exe, the .NET Framework host that ships with Visual Studio. # Same engine, different host. # - No separate restore pass. `dotnet build` and `dotnet publish` restore implicitly, and the # solution filter build restores every nuget project in one go. # - No per project obj/bin wipe. Clear-Solution gives the clean slate, and the nuspec files list # their content explicitly, so nothing stale can end up in a package. The wipe used to force a # full restore and a rebuild of the whole ProjectReference chain for every single config. # - -ShowTiming prints the per step timing report at any verbosity, -BinaryLog writes one binary # build log (.binlog) per build step, readable with the MSBuild Structured Log Viewer. # - -Verbosity quiet still shows progress: the engine runs at minimal, so it prints one line per project # as it completes (its terminal logger's line in a terminal, "Project -> output" in a log), and every # package gets its own "#### APT PACK <file>.nupkg ####" line. # # Packages are still defined by the hand written .nuspec files, not by the csproj: `dotnet pack` is # given the nuspec through NuspecFile, which makes it authoritative for id, version, dependencies and # the explicit file list, and NuspecProperties feeds the $configuration$ token exactly like # `nuget pack -properties` did. Same package content, no nuget.exe - the .NET SDK is the only tool # this module needs. # # Only what the json configs describe gets built. A project without a config is never loaded, unless # it is a ProjectReference of one that has - then it is built as a requirement, nothing more. # # function Get-Absolute-Path([string] $path){ if($path.StartsWith(".")){ $path = [IO.Path]::Combine((Get-Location).Path, $path) } return [IO.Path]::GetFullPath($path) } # function Get-Relative-Path([string] $from, [string] $to){ # $to relative to directory $from, with backslashes. Fast path for the usual case (project below # the solution), Uri based otherwise. [IO.Path]::GetRelativePath does not exist on PowerShell 5.1. $root = $from.TrimEnd('\', '/') + '\' if($to.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)){ return $to.Substring($root.Length) } $relative = (New-Object System.Uri($root)).MakeRelativeUri((New-Object System.Uri($to))).ToString() return [Uri]::UnescapeDataString($relative).Replace('/', '\') } # function Get-Custom-Property([pscustomobject] $property){ # One "/p:Name=Value" argument per property, meant to be splatted (@customProperties) so dotnet # gets each one as its own argument. Joined into a single string, a second property would end up # inside the first property's value. # # Callers MUST wrap the result in @( ): PowerShell unrolls a one element array coming out of a # function into a plain string, and splatting a string hands the native command its CHARACTERS # ("/", "p", ":", ...) - MSBuild then fails with MSB1001 Unknown switch "/". if($null -eq $property){ return @() } return @($property.PSObject.Properties | ForEach-Object { "/p:$($_.Name)=$($_.Value)" }) } # function Get-Binlog-Argument([string] $directory, [int] $step, [string] $name){ # "-bl:<directory>\<step>-<name>.binlog" or nothing. One file per build step - the build engine # overwrites a binlog it is pointed at twice. Wrap the result in @( ), see Get-Custom-Property. if([string]::IsNullOrEmpty($directory)){ return @() } $safe = $name -replace '[^A-Za-z0-9._-]', '_' return @("-bl:$([IO.Path]::Combine($directory, ('{0:D2}-{1}.binlog' -f $step, $safe)))") } # function Get-Max-Cpu-Count(){ # dotnet build/publish defaults to ALL logical processors. Memory scales with the node count, so # this is the knob to turn on a small machine: APT_MAXCPUCOUNT overrides the detected value. $cores = [Environment]::ProcessorCount if(-not [string]::IsNullOrEmpty($env:APT_MAXCPUCOUNT)){ return [math]::Max(1, [math]::Min([int] $env:APT_MAXCPUCOUNT, $cores)) } # clamped to the declared MaxCpuCount range - ValidateRange is re-checked on assignment return [math]::Max(1, [math]::Min($cores, 64)) } # function Test-Projects-DotnetBuildable([string[]] $projects){ # The dotnet CLI cannot build everything MSBuild.exe can: # - legacy non SDK csproj is the old project format, whatever it targets (net48 included) # - packages.config needs classic restore, dotnet restore only does PackageReference # The projects here are csproj by construction (a config only counts next to a csproj), so the # sqlproj / vcxproj family never reaches this point - the solution filter leaves them out. $sdkStyle = '<Project[^>]*\sSdk\s*=|<Sdk\s+Name\s*=|<Import[^>]*\sSdk\s*=' $blocked = [System.Collections.Generic.List[string]]::new() foreach($project in $projects){ $name = Split-Path -Leaf $project if($project -notlike '*.csproj'){ $blocked.Add("$name [needs MSBuild.exe]") Continue } if(-not (Test-Path -Path $project -PathType Leaf)){ $blocked.Add("$name [not found]") Continue } if((Get-Content -Path $project -Raw) -notmatch $sdkStyle){ $blocked.Add("$name [legacy project format]") Continue } if(Test-Path -Path ([IO.Path]::Combine((Split-Path -Parent $project), 'packages.config')) -PathType Leaf){ $blocked.Add("$name [packages.config]") } } if($blocked.Count -gt 0){ Write-Host "#### APT BUILD per project, not dotnet buildable: $($blocked -join ', ') ####" return $false } return $true } # function New-Solution-Filter([string] $solution, [string[]] $projects, [string] $configuration){ # A solution filter is a json file next to the solution naming the projects to load from it. # `dotnet build x.slnf` behaves like a solution build (SolutionDir etc. are set, one restore for # everything) but only for the listed projects - ProjectReferences still build transitively. $solutionPath = Split-Path -Parent $solution $name = [IO.Path]::GetFileNameWithoutExtension($solution) $filter = [IO.Path]::Combine($solutionPath, "$name.apt.$configuration.slnf") [string[]] $relative = @($projects | ForEach-Object { Get-Relative-Path $solutionPath $_ } | Sort-Object -Unique) $content = @{ solution = @{ path = (Split-Path -Leaf $solution); projects = $relative } } | ConvertTo-Json -Depth 4 Set-Content -Path $filter -Value $content -Encoding UTF8 return $filter } # function Add-Build-Timing($timings, [string] $name, [string] $phase, $stopwatch){ $stopwatch.Stop() $timings.Add([pscustomobject]@{ Seconds = [math]::Round($stopwatch.Elapsed.TotalSeconds, 1); Phase = $phase; Project = $name }) } # function Write-Build-Timing($timings){ if($timings.Count -eq 0){ return } $total = ($timings | Measure-Object -Property Seconds -Sum).Sum Write-Host "#### APT TIMING slowest steps ####" ($timings | Sort-Object -Property Seconds -Descending | Select-Object -First 12 | Format-Table -AutoSize | Out-String).Trim() | Write-Host Write-Host "#### APT TIMING by phase ####" ($timings | Group-Object -Property Phase | Select-Object @{N='Phase';E={$_.Name}}, @{N='Count';E={$_.Count}}, @{N='Seconds';E={[math]::Round(($_.Group | Measure-Object -Property Seconds -Sum).Sum, 1)}} | Sort-Object -Property Seconds -Descending | Format-Table -AutoSize | Out-String).Trim() | Write-Host Write-Host ("#### APT TIMING measured {0:N1}s across {1} steps ####" -f $total, $timings.Count) } # function Get-Default-Settings($debug, $configuration, [string] $publishPath, [string] $project){ # Fills a config section (Publish entry or Nuget section) up with defaults. Note that the # Configuration in the json is only honoured together with -EnableDebug; a normal run is Release. $setting = @{} # $setting.Enabled = $true if($null -ne $configuration.Enabled){ $setting.Enabled = $configuration.Enabled } # $setting.Configuration = "Release" if($debug -and ($null -ne $configuration.Configuration)){ $setting.Configuration = $configuration.Configuration } # $setting.Runtime = "linux-x64" if($null -ne $configuration.Runtime){ $setting.Runtime = $configuration.Runtime } # $setting.SelfContained = $false if($null -ne $configuration.SelfContained){ $setting.SelfContained = $configuration.SelfContained } # $setting.SingleFile = $false if($null -ne $configuration.SingleFile){ $setting.SingleFile = $configuration.SingleFile } # publish output: config Path, else the project name, else (nuget) the publish root itself $path = $configuration.Path if([string]::IsNullOrEmpty($path)){ $path = "" } if([string]::IsNullOrEmpty($path) -and (-not [string]::IsNullOrEmpty($project))){ $path = [IO.Path]::GetFileNameWithoutExtension($project) } $setting.Path = [IO.Path]::Combine($publishPath, $path) # $setting.Properties = $configuration.Properties return $setting } <# .Synopsis Automatic Publish Tool (APT) .DESCRIPTION Publish a Visual Studio solution with the dotnet CLI only (no MSBuild.exe, no Visual Studio needed). Drop in compatible with Publish-Application: same parameters, same lomtec/nuaktiv.json files. .EXAMPLE Publish-DotnetApplication D:\Projects\project.sln D:\Project\publish\admin .EXAMPLE Publish-DotnetApplication D:\Projects\project.sln D:\Project\publish -ShowTiming -BinaryLog D:\Temp\binlog .NOTES Author: Imrich Szolik #> function Publish-DotnetApplication { [CmdletBinding()] Param( # Solution file (sln) [string] [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$false, HelpMessage="Solution file.")] $Solution, #Publish path. [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$false, HelpMessage="Publish path.")] [string] $PublishPath, #config [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, HelpMessage="Publish json config")] [string] $JsonConfig = "(lomtec|nuaktiv)\.json", #verbosity [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, HelpMessage="Build verbosity. quiet still prints one line per project as it is built (the engine runs at minimal)")] [ValidateSet('quiet','minimal','normal','detailed', 'diagnostic')] [string] $Verbosity = "quiet", #platform - filters Publish configs by their Runtime (linux* / win*). 'all' publishes everything. [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, HelpMessage="Build platform")] [ValidateSet('all','linux','windows')] [string] $Platform = "all", #max cpu [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, HelpMessage="Maximum parallel MSBuild nodes (0 = all processors available to this machine)")] [ValidateRange(0,64)] [int] $MaxCpuCount = 0, #enable debug mode [switch] [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, HelpMessage="Enable Debug compilation")] $EnableDebug, #keep the roslyn compiler server alive [switch] [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, HelpMessage="Reuse the Roslyn compiler server (faster, but one long lived process holds memory until the build server shutdown)")] $EnableSharedCompilation, #timing report at any verbosity [switch] [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, HelpMessage="Print the per step timing report (always printed at -Verbosity diagnostic)")] $ShowTiming, #binary build logs (the .binlog format of the build engine inside the dotnet CLI - no MSBuild.exe involved) [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, HelpMessage="Directory for binary build logs, one .binlog per build step (open with the MSBuild Structured Log Viewer)")] [string] $BinaryLog ) Begin{ #required - the dotnet SDK, and nothing else if ($null -eq (Get-Command dotnet -ErrorAction SilentlyContinue)) { throw "Please install dotnet SDK. [ Download https://dotnet.microsoft.com/en-us/download ]" } #solution if( (-not (Test-Path -Path $Solution -PathType Leaf)) -or (-not ( $Solution -like '*.sln')) ){ throw "$Solution is not valid" } $solution = ( Resolve-Path $Solution ).Path $solutionPath = Split-Path -Parent $solution # $PublishPath = Get-Absolute-Path $PublishPath # if(-not [string]::IsNullOrEmpty($BinaryLog)){ $BinaryLog = Get-Absolute-Path $BinaryLog New-Item -ItemType Directory -Path $BinaryLog -Force | Out-Null } #max cpu (dotnet defaults to ALL logical cores, so this always has to be set) if($MaxCpuCount -le 0){ $MaxCpuCount = Get-Max-Cpu-Count } #shared compilation is off by default: the roslyn server is one more process holding memory for #the whole build. On a single solution build it is worth about 25% of the compile time. $sharedCompilation = "-p:UseSharedCompilation=$($EnableSharedCompilation.IsPresent)" Write-Host "#### APT BUILD dotnet CLI, parallel nodes [-m:$MaxCpuCount], shared compilation [$($EnableSharedCompilation.IsPresent)] ####" #the engine's verbosity. At 'quiet' the engine prints nothing at all while a 30 second filter build #runs, so quiet is mapped to minimal: one line per project as it completes ("Project -> output.dll"), #warnings stay visible. Every other verbosity is passed through as given. The timing report gate #below still looks at the verbosity that was asked for. $engineVerbosity = if($Verbosity -eq 'quiet'){ 'minimal' } else { $Verbosity } } Process{ $timings = [System.Collections.Generic.List[object]]::new() $filters = [System.Collections.Generic.List[string]]::new() try{ $binlogStep = 0 #### 1. plan #################################################################################### #config files - anything matching $JsonConfig that sits next to a csproj $lomtecConfigs = @(Get-ChildItem -Path $solutionPath -Recurse -File -Filter *.json | Where-Object { ($_.Name -Match $JsonConfig) -and ($_.FullName -notmatch '[\\/](node_modules|bin|obj)[\\/]') } | Select-Object -ExpandProperty FullName) if($lomtecConfigs.Count -eq 0){ throw "Not found any build configuration [$JsonConfig]!" } # $plans = [System.Collections.Generic.List[object]]::new() foreach($lomtec in $lomtecConfigs){ #the project the config belongs to $project = Get-ChildItem (Split-Path -Parent $lomtec) -Filter "*.csproj" | Select-Object -ExpandProperty FullName -First 1 if($null -eq $project){ Write-Verbose "Missing csproj file for [$lomtec]" Continue } $projectName = Split-Path -Leaf $project #json $json = Get-Content $lomtec | ConvertFrom-Json if($null -eq $json.Build){ Write-Warning "Missing Build section for [$lomtec]" Continue } #no MSBuild.exe in here - say so once per config that asks for it, then build with dotnet if($json.Build.Command -eq 'msbuild'){ Write-Warning "[$projectName] asks for msbuild, Publish-DotnetApplication builds it with the dotnet CLI" } #publish entries if($null -ne $json.Build.Publish -and @($json.Build.Publish).Count -gt 0){ foreach($config in $json.Build.Publish){ $setting = Get-Default-Settings $EnableDebug.IsPresent $config $PublishPath $project Write-Verbose " Configuration $($setting | ConvertTo-Json -Compress)" #platform filter if($Platform -eq 'linux' -and $setting.Runtime -notlike "linux*") { Continue } if($Platform -eq 'windows' -and $setting.Runtime -notlike "win*") { Continue } # if($setting.Enabled -eq $false){ Write-Warning "#### APT PUBLISH $projectName [$(Split-Path -Leaf $setting.Path)] is not enabled ####" Continue } $plans.Add([pscustomobject]@{ Kind = 'publish'; Project = $project; Name = $projectName; Setting = $setting; Config = $lomtec; Nuspec = $null }) } } #nuget package elseif($null -ne $json.Build.Nuget){ #no project name in the path, every package lands in the publish root $setting = Get-Default-Settings $EnableDebug.IsPresent $json.Build.Nuget $PublishPath Write-Verbose " Configuration $($setting | ConvertTo-Json -Compress)" if($setting.Enabled -eq $false){ Write-Warning "Build/Nuget is disabled for $projectName" Continue } $plans.Add([pscustomobject]@{ Kind = 'nuget'; Project = $project; Name = $projectName; Setting = $setting; Config = $lomtec; Nuspec = $project.Replace("csproj", "nuspec") }) } else{ Write-Warning "Missing Build/Publish or Build/Nuget section for [$lomtec]!" } } if($plans.Count -eq 0){ Write-Warning "Nothing to build - every configuration is disabled or filtered out" return } #### 2. validate ################################################################################ #everything that can be checked without building is checked now, and reported in one go $problems = [System.Collections.Generic.List[string]]::new() foreach($plan in $plans){ if([string]::IsNullOrWhiteSpace($plan.Setting.Configuration)){ $problems.Add("$($plan.Name): empty Configuration [$($plan.Config)]") } #a missing nuspec is a warning, not an error: the project still builds, it is just not packed if($plan.Kind -eq 'nuget' -and -not (Test-Path -Path $plan.Nuspec -PathType Leaf)){ Write-Warning "$($plan.Name): Build/Nuget is configured but $($plan.Nuspec) is missing - the project is built, no package is produced" $plan.Nuspec = $null } if($plan.Kind -eq 'publish' -and $plan.Setting.Runtime -notmatch '^[a-z][a-z0-9.]*(-[a-z0-9.]+)*$'){ $problems.Add("$($plan.Name): '$($plan.Setting.Runtime)' does not look like a runtime identifier [$($plan.Config)]") } } #two publishes into the same folder would overwrite each other $plans | Where-Object Kind -eq 'publish' | Group-Object { $_.Setting.Path.ToLowerInvariant() } | Where-Object Count -gt 1 | ForEach-Object { $problems.Add("publish path used more than once: $($_.Group[0].Setting.Path) <- $(($_.Group | ForEach-Object { $_.Name }) -join ', ')") } if($problems.Count -gt 0){ throw "APT configuration is not valid, nothing was built:`n - $($problems -join "`n - ")" } Write-Host "#### APT PLAN $(@($plans | Where-Object { $_.Kind -eq 'nuget' -and $null -ne $_.Nuspec }).Count) package(s), $(@($plans | Where-Object Kind -eq 'publish').Count) publish target(s) ####" #### 3. build ################################################################################### #nuget projects without custom properties share one solution filter build per configuration. A #plan with custom properties builds on its own, because those properties only apply to it. $solutionBuilt = @{} $shared = @($plans | Where-Object { $_.Kind -eq 'nuget' -and @(Get-Custom-Property $_.Setting.Properties).Count -eq 0 }) if($shared.Count -gt 0 -and (Test-Projects-DotnetBuildable @($shared | ForEach-Object { $_.Project } | Sort-Object -Unique))){ foreach($group in ($shared | Group-Object { $_.Setting.Configuration })){ $configuration = $group.Name $filter = New-Solution-Filter $solution @($group.Group | ForEach-Object { $_.Project }) $configuration $filters.Add($filter) Write-Host "#### APT BUILD $($group.Count) project(s) [$configuration] via $(Split-Path -Leaf $filter) [dotnet] ####" $binlog = @(Get-Binlog-Argument $BinaryLog (++$binlogStep) "build-$configuration") $sw = [Diagnostics.Stopwatch]::StartNew() dotnet build -nologo /nodeReuse:false -m:$MaxCpuCount $sharedCompilation --verbosity $engineVerbosity --configuration $configuration @binlog $filter if($LastExitCode) { throw "DOTNET BUILD $(Split-Path -Leaf $filter) failed" } Add-Build-Timing $timings (Split-Path -Leaf $filter) 'build solution' $sw $solutionBuilt[$configuration] = $true } } #whatever the solution filter did not cover builds on its own foreach($plan in ($plans | Where-Object Kind -eq 'nuget')){ $customProperties = @(Get-Custom-Property $plan.Setting.Properties) if(($customProperties.Count -eq 0) -and $solutionBuilt.ContainsKey($plan.Setting.Configuration)){ Continue } Write-Host "#### APT BUILD $($plan.Name) [$($plan.Setting.Configuration)] [dotnet] ####" $binlog = @(Get-Binlog-Argument $BinaryLog (++$binlogStep) "build-$($plan.Name)") $sw = [Diagnostics.Stopwatch]::StartNew() dotnet build -nologo /nodeReuse:false -m:$MaxCpuCount $sharedCompilation --verbosity $engineVerbosity --configuration $plan.Setting.Configuration @customProperties @binlog $plan.Project if($LastExitCode) { throw "DOTNET BUILD $($plan.Name) failed" } Add-Build-Timing $timings $plan.Name 'build' $sw } #### 4. publish ################################################################################# #publish restores implicitly - it needs the runtime specific (RID) restore anyway foreach($plan in ($plans | Where-Object Kind -eq 'publish')){ $setting = $plan.Setting $customProperties = @(Get-Custom-Property $setting.Properties) Write-Host "#### APT PUBLISH $($plan.Name) to $($setting.Path) [dotnet] ####" $binlog = @(Get-Binlog-Argument $BinaryLog (++$binlogStep) "publish-$(Split-Path -Leaf $setting.Path)") $sw = [Diagnostics.Stopwatch]::StartNew() dotnet publish -nologo /nodeReuse:false -m:$MaxCpuCount $sharedCompilation --verbosity $engineVerbosity --runtime $setting.Runtime --configuration $setting.Configuration --self-contained $setting.SelfContained -p:PublishSingleFile=$($setting.SingleFile) @customProperties @binlog $plan.Project --output $setting.Path if($LastExitCode) { throw "DOTNET PUBLISH $($plan.Name) failed" } Add-Build-Timing $timings $plan.Name 'publish' $sw } #### 5. pack #################################################################################### #dotnet pack driven by the nuspec: NuspecFile makes the nuspec authoritative (id, version, #dependencies, files - the csproj metadata is not used) and NuspecProperties fills in the #$configuration$ token. --no-build, everything is built already. Each pack is one project, so it #gets one msbuild node (-m:1); the parallelism comes from running the packs side by side. #Everything the pack block needs travels inside the queue item, so the same block works both #in ForEach-Object -Parallel (where only $using: would reach outer variables) and sequentially. $packQueue = @($plans | Where-Object { $_.Kind -eq 'nuget' -and $null -ne $_.Nuspec } | ForEach-Object { [pscustomobject]@{ Name = $_.Name; Project = $_.Project; Nuspec = $_.Nuspec; Output = $_.Setting.Path Configuration = $_.Setting.Configuration; Verbosity = $engineVerbosity; BinaryLog = $BinaryLog } }) if($packQueue.Count -gt 0){ Write-Host "#### APT PACK $($packQueue.Count) package(s) to $PublishPath [dotnet] ####" $sw = [Diagnostics.Stopwatch]::StartNew() $pack = { $item = $_ $binlog = @() if(-not [string]::IsNullOrEmpty($item.BinaryLog)){ $binlog = @("-bl:$([IO.Path]::Combine($item.BinaryLog, ('pack-{0}.binlog' -f ($item.Name -replace '[^A-Za-z0-9._-]', '_'))))") } $output = dotnet pack $item.Project --no-build --nologo /nodeReuse:false -m:1 --verbosity $item.Verbosity --configuration $item.Configuration --output $item.Output "-p:NuspecFile=$($item.Nuspec)" "-p:NuspecBasePath=$(Split-Path -Parent $item.Project)" "-p:NuspecProperties=configuration=$($item.Configuration)" -p:IsPackable=true @binlog 2>&1 if($LASTEXITCODE){ return "$($item.Name) [dotnet pack exit $LASTEXITCODE] $((@($output) | Select-Object -Last 3) -join ' | ')" } #the output is captured for the failure message above, so show the one line worth seeing - #as "Successfully created [<file>.nupkg]." with the file name only, no path @($output) | Where-Object { "$_" -match "Successfully created package '(?<path>[^']+)'" } | ForEach-Object { [void]("$_" -match "Successfully created package '(?<path>[^']+)'") Write-Host "#### APT PACK $(Split-Path -Leaf $Matches.path) ####" } #exit 0 is not proof of a package: a project that is not packable is skipped silently $manifest = New-Object System.Xml.XmlDocument $manifest.Load($item.Nuspec) $expected = [IO.Path]::Combine($item.Output, "$($manifest.package.metadata.id).$($manifest.package.metadata.version).nupkg") if(-not (Test-Path -Path $expected -PathType Leaf)){ return "$($item.Name) [$(Split-Path -Leaf $expected) was not produced]" } } #ForEach-Object -Parallel needs PowerShell 7, keep a sequential path for 5.1 if($PSVersionTable.PSVersion.Major -ge 7){ $packFailed = $packQueue | ForEach-Object -ThrottleLimit $MaxCpuCount -Parallel $pack } else{ $packFailed = $packQueue | ForEach-Object $pack } Add-Build-Timing $timings "$($packQueue.Count) package(s)" 'pack' $sw if(@($packFailed).Count -gt 0){ throw "DOTNET PACK failed:`n - $(@($packFailed) -join "`n - ")" } } #### 6. cleanup ################################################################################# #nothing from the build environment ships in the publish output Write-Verbose "Publish cleanup [$PublishPath]" Get-ChildItem -Path $PublishPath -Recurse -Filter *.pdb -File -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue Get-ChildItem -Path $PublishPath -Recurse -Filter $JsonConfig -File -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue Get-ChildItem -Path $PublishPath -Recurse -Filter appsettings.*.json -File -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue Get-ChildItem -Path $PublishPath -Recurse -Filter web.*.config -File -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue } finally{ #timing report if($ShowTiming.IsPresent -or ($Verbosity -eq 'diagnostic')){ Write-Build-Timing $timings } #the generated solution filters are build artefacts, not source foreach($filter in $filters){ Remove-Item -Path $filter -Force -ErrorAction SilentlyContinue } #release build servers (msbuild nodes, roslyn compiler, razor) from memory Write-Verbose "Build server shutdown" dotnet build-server shutdown | Out-Null $global:LASTEXITCODE = 0 } } End{ } } Export-ModuleMember -Function Publish-DotnetApplication # SIG # Begin signature block # MIIIEwYJKoZIhvcNAQcCoIIIBDCCCAACAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA1nG9ZErTxmsH7 # 9dkmo1qEOXQ0PxpS+uGMo2AH+Ng0DaCCBQ8wggULMIIC86ADAgECAgIApTANBgkq # hkiG9w0BAQsFADCBoDELMAkGA1UEBhMCU0sxETAPBgNVBAgTCFNsb3Zha2lhMRMw # EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpMb210ZWMuY29tMSswKQYDVQQD # EyJMb210ZWMuY29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MScwJQYJKoZIhvcN # AQkBFhhJbXJpY2guU3pvbGlrQGxvbXRlYy5jb20wHhcNMTkwMzI2MTUwNDQzWhcN # MzAwMTAxMDAwMDAwWjCBkzELMAkGA1UEBhMCU0sxETAPBgNVBAgTCFNsb3Zha2lh # MRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpMb210ZWMuY29tMR4wHAYD # VQQDExVMb210ZWMuY29tIFBvd2Vyc2hlbGwxJzAlBgkqhkiG9w0BCQEWGEltcmlj # aC5Tem9saWtAbG9tdGVjLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC # ggEBAL4n4nCHbkdrf09IHNFQ2P/z6I43GKScsFJOQHUMkRD3ALoUFL/URC9sW2fY # rqG5VkFAKhnM0VxeiICR53cAshNShjFf58PhaS973jtCoJcaugIVBVoFIuQ+gnNY # Jp2VdBbIPKMW4JjjZOxBEkHpMWmfitXKWGKeU68Qcn3oI6PO+YSztfXLe1NU+1GI # O3fA7E0vHVPUf/qWZXMYU5ElLQVm9AXbfX79mTgl76A57+OC6j+Aehkd2OPfUl4w # snox3fOyUAUA8iojeWh97PpXd/s+RkuxWdgsC4YSWDUjhZSzBkml9uerYqo9a+XA # b39dvkpK9TPl3q5HNBQMkCfp8bkCAwEAAaNaMFgwCQYDVR0TBAIwADALBgNVHQ8E # BAMCB4AwKwYDVR0lBCQwIgYIKwYBBQUHAwMGCisGAQQBgjcCARUGCisGAQQBgjcC # ARYwEQYJYIZIAYb4QgEBBAQDAgSwMA0GCSqGSIb3DQEBCwUAA4ICAQAJJapv9skY # jh5HTsJnqDdtqh7YOOXuA8g+DKBj+5gDEZE5V9VhAFVp8UJ9RoITGGIRTVId0lqc # LJiVSTHx305VW9aID8vo77kfrTyXvPXNIsTtHnPkkwH47+CoiY3IpPQLjUA/Q6Qd # qwINvvwom7/Wc+OoIqlPdJH5DbBrIy85dr6M/Lm3Rw2BolcTRwXTB3xAhweth78B # P6pbcAd32FdymkRopLIihuNs7g7ZR/Q/5803G+OiQIMRGyTvaQ+aQjJgFpkzp7NI # whzougfCOV47Sc89jEpUqw16i2UFfz2ywOlWUyYtue1S1PjM1ljgJfRo+e0wUnFp # gFQzXGF1bTYVaQ4e3nJleADfvqeXoH2AYeBTbz9BcogkkfURAC3iiob1bNs5jE1C # brEDCw6m/03k0oOmm3xQksXyAhBYuUkRwu9jd4y3FwZ/syDGLz3b6cY8o1YyINOO # A3B2r92shNt0rWhJu3v+qcIVmFQ0aKlhNNRoiVlQgJ7NgO0UV+vU2lgiscpUSxEt # xKN+450r49su06NA6zsyn3CELUmVkPyjx5fyizwt9KxVuYOUSEb32Y7QCffHJ1qt # F6SuQrKbgb/24y7cCDW6PDVRvPOySUlKu9sPykICDjvzXBvjYEILM7AmtYCIBGQg # imBsQvTEoXJXiwAOi1XNz3LxcjQHQ/ZM4zGCAlowggJWAgEBMIGnMIGgMQswCQYD # VQQGEwJTSzERMA8GA1UECBMIU2xvdmFraWExEzARBgNVBAcTCkJyYXRpc2xhdmEx # EzARBgNVBAoTCkxvbXRlYy5jb20xKzApBgNVBAMTIkxvbXRlYy5jb20gQ2VydGlm # aWNhdGlvbiBBdXRob3JpdHkxJzAlBgkqhkiG9w0BCQEWGEltcmljaC5Tem9saWtA # bG9tdGVjLmNvbQICAKUwDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEK # MAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3 # AgELMQ4wDAYKKwYBBAGCNwIBFjAvBgkqhkiG9w0BCQQxIgQgstawU8zRsMNlD4wU # TtRZnMDbJ/k5fPcybilN5nAMfjQwDQYJKoZIhvcNAQEBBQAEggEAT7AOoBVoLJkP # sgyxPzKnxzlGPtl0phUjfmnZp6iHMvH+zCOHwItWPFGJl3zdgqq4ygvHJOamlivX # 1yWGR8RdmYrEj0QJ95Cy0Vmo+bMY1+mxVMV4KUg/OQ80G3PIkY9yR3Q2bdvcQf+9 # qUGqCCBZUoWfelZrvhDB+JycsfCBt7MW0TZdbfYKk1Nra65mvAvS7BYX555T/DxV # 0XKsq5I02DnxfvXGoIHUsBKWrQHc1peQPEmhVrMafb8Q092B0rcI19XOBSJyDg2U # +32qJMb95VgdBxdpSqAMrnL7/IfpsSbKA3RjQjacUyIvph6BvNGFrLuaiYifrGc+ # s4/7ifefIw== # SIG # End signature block |