Skip to content

Commit 6d00d0a

Browse files
aholstrup1Copilot
andauthored
Add support for workspace compilation (#2161)
### ❔What, Why & How <!-- Include description of the changes that will help reviewers in their task --> With v28 of BC the ALTool now supports workspace compilation. The key advantage of workspace compilation is that it supports parallel compilation which can significantly improve the overall compilation time for large AL-Go projects. This PR adds AL-Go support for workspace compilation by adding a new "Compile" action. Thereby also moving the compilation out of Run-ALPipeline. **Limitations** Limitations that aren't addressed in this PR but are on our radar to fix in follow-up PRs: * Support for Directory.packages.props * Support for compilation on linux (Status: Bug on Developer Experiences) * Support for incremental builds * Support for compiling BCPT apps * Support for "Previous Apps" app source validation (i.e. apps from releases used as baseline) Related to: [AB#618073](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/618073) ### ✅ Checklist - [x] Add tests (E2E, unit tests) - [x] Update RELEASENOTES.md - [x] Update documentation (e.g. for new settings or scenarios) - [x] Add telemetry <!-- Include more checklist entries, if needed --> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 1815368 commit 6d00d0a

17 files changed

Lines changed: 2500 additions & 94 deletions

File tree

Actions/.Modules/CompileFromWorkspace.psm1

Lines changed: 855 additions & 0 deletions
Large diffs are not rendered by default.

Actions/.Modules/ReadSettings.psm1

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ function GetDefaultSettings
161161
"enableUICop" = $false
162162
"enableCodeAnalyzersOnTestApps" = $false
163163
"customCodeCops" = @()
164+
"preprocessorSymbols" = @()
165+
"features" = @()
164166
"trackALAlertsInGitHub" = $false
165167
"failOn" = "error"
166168
"treatTestFailuresAsWarnings" = $false
@@ -211,6 +213,10 @@ function GetDefaultSettings
211213
"environments" = @()
212214
"buildModes" = @()
213215
"useCompilerFolder" = $false
216+
"workspaceCompilation" = [ordered]@{
217+
"enabled" = $false
218+
"parallelism" = 1
219+
}
214220
"pullRequestTrigger" = "pull_request"
215221
"bcptThresholds" = [ordered]@{
216222
"DurationWarning" = 10
@@ -564,6 +570,11 @@ function ReadSettings {
564570
$settings.projectName = $project # Default to project path as project name
565571
}
566572

573+
# Interpret zero or negative parallelism as the max number of processors
574+
if ($settings.workspaceCompilation.parallelism -le 0) {
575+
$settings.workspaceCompilation.parallelism = [System.Environment]::ProcessorCount
576+
}
577+
567578
$settings | ValidateSettings
568579

569580
$settings

Actions/.Modules/settings.schema.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,20 @@
297297
},
298298
"description": "See https://aka.ms/ALGoSettings#customcodecops"
299299
},
300+
"preprocessorSymbols": {
301+
"type": "array",
302+
"items": {
303+
"type": "string"
304+
},
305+
"description": "Preprocessor symbols to use when compiling the app. See https://aka.ms/ALGoSettings#preprocessorsymbols"
306+
},
307+
"features": {
308+
"type": "array",
309+
"items": {
310+
"type": "string"
311+
},
312+
"description": "Features to enable when compiling the app. See https://aka.ms/ALGoSettings#features"
313+
},
300314
"trackALAlertsInGitHub": {
301315
"type": "boolean",
302316
"description": "Enable tracking of AL Alerts in GitHub. See https://aka.ms/ALGoSettings#trackALAlertsInGitHub"
@@ -518,6 +532,21 @@
518532
"type": "boolean",
519533
"description": "Use the compiler folder instead of a BC container. See https://aka.ms/ALGoSettings#usecompilerfolder"
520534
},
535+
"workspaceCompilation": {
536+
"type": "object",
537+
"description": "PREVIEW: Configuration for workspace compilation. See https://aka.ms/ALGoSettings#workspacecompilation",
538+
"properties": {
539+
"enabled": {
540+
"type": "boolean",
541+
"description": "Enable workspace compilation for building apps"
542+
},
543+
"parallelism": {
544+
"type": "integer",
545+
"description": "The number of parallel processes to use for workspace compilation. Set to 0 or -1 to use all available processors."
546+
}
547+
},
548+
"additionalProperties": false
549+
},
521550
"pullRequestTrigger": {
522551
"type": "string",
523552
"pattern": "^(pull_request|pull_request_target)$",

Actions/AL-Go-Helper.ps1

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,42 @@ $runAlPipelineOverrides = @(
5454
"PipelineFinalize"
5555
)
5656

57+
<#
58+
.SYNOPSIS
59+
Gets script overrides from the AL-Go folder.
60+
.DESCRIPTION
61+
Checks the specified AL-Go folder for .ps1 scripts matching the provided override names.
62+
Returns a hashtable mapping each found override name to its script block.
63+
.PARAMETER ALGoFolderName
64+
The folder where the AL-Go scripts are located.
65+
.PARAMETER OverrideScriptNames
66+
An array of script names to look for (without .ps1 extension).
67+
.OUTPUTS
68+
Hashtable with override script names as keys and their script blocks as values.
69+
#>
70+
function Get-ScriptOverrides() {
71+
param(
72+
[Parameter(Mandatory = $true)]
73+
[string] $ALGoFolderName,
74+
[Parameter(Mandatory = $true)]
75+
[string[]] $OverrideScriptNames
76+
)
77+
$overrides = @{}
78+
foreach ($scriptName in $OverrideScriptNames) {
79+
$scriptPath = Join-Path $ALGoFolderName "$scriptName.ps1"
80+
if (Test-Path -Path $scriptPath -Type Leaf) {
81+
OutputDebug "Add override for $scriptName ($scriptPath)"
82+
Trace-Information -Message "Using override for $scriptName"
83+
$scriptBlock = (Get-Command $scriptPath | Select-Object -ExpandProperty ScriptBlock)
84+
if (-not $scriptBlock) {
85+
OutputError -message "Failed to get scriptblock for $scriptName.ps1, please check the override for validity."
86+
}
87+
$overrides[$scriptName] = $scriptBlock
88+
}
89+
}
90+
return $overrides
91+
}
92+
5793
# Well known AppIds
5894
$platformAppId = "8874ed3a-0643-4247-9ced-7a7002f7135d"
5995
$systemAppId = "63ca2fa4-4f03-4f2b-a480-172fef340d3f"
@@ -1570,16 +1606,7 @@ function CreateDevEnv {
15701606

15711607
Push-Location $projectFolder
15721608
try {
1573-
$runAlPipelineOverrides | ForEach-Object {
1574-
$scriptName = $_
1575-
$scriptPath = Join-Path $ALGoFolderName "$ScriptName.ps1"
1576-
if (Test-Path -Path $scriptPath -Type Leaf) {
1577-
Write-Host "Add override for $scriptName"
1578-
$runAlPipelineParams += @{
1579-
"$scriptName" = (Get-Command $scriptPath | Select-Object -ExpandProperty ScriptBlock)
1580-
}
1581-
}
1582-
}
1609+
$runAlPipelineParams += (Get-ScriptOverrides -ALGoFolderName $ALGoFolderName -OverrideScriptNames $runAlPipelineOverrides)
15831610

15841611
if ($kind -eq "local") {
15851612
$runAlPipelineParams += @{
@@ -2411,3 +2438,49 @@ function RunAndCheck {
24112438
throw "$($args[0]) $($rest | ForEach-Object { $_ }) failed with exit code $LASTEXITCODE"
24122439
}
24132440
}
2441+
2442+
<#
2443+
.SYNOPSIS
2444+
Get the version number components based on the versioning strategy
2445+
.DESCRIPTION
2446+
Get the version number components based on the versioning strategy defined in the settings.
2447+
.PARAMETER Settings
2448+
The settings object containing versioning information.
2449+
.RETURNS
2450+
A PSCustomObject with MajorMinorVersion, BuildNumber, and RevisionNumber properties.
2451+
#>
2452+
function Get-VersionNumber() {
2453+
param(
2454+
[Parameter(Mandatory=$true)]
2455+
$Settings
2456+
)
2457+
$majorMinorVersion = ""
2458+
$appBuild = $Settings.appBuild
2459+
$appRevision = $Settings.appRevision
2460+
2461+
if ($Settings.versioningStrategy -eq -1) {
2462+
$artifactVersion = [Version]$Settings.artifact.Split('/')[4]
2463+
$majorMinorVersion = "$($artifactVersion.Major).$($artifactVersion.Minor)"
2464+
$appBuild = $artifactVersion.Build
2465+
$appRevision = $artifactVersion.Revision
2466+
} elseif (($Settings.versioningStrategy -band 16) -eq 16) {
2467+
# For versioningStrategy +16, the version number is taken from repoVersion setting
2468+
$repoVersion = [System.Version]$Settings.repoVersion
2469+
$majorMinorVersion = "$($repoVersion.Major).$($repoVersion.Minor)"
2470+
if (($Settings.versioningStrategy -band 15) -eq 3) {
2471+
# For versioning strategy 3, we need to get the build number from repoVersion setting
2472+
$appBuild = $repoVersion.Build
2473+
if ($appBuild -eq -1) {
2474+
OutputWarning -message "RepoVersion setting only contains Major.Minor version. When using versioningStrategy 3, it should contain 3 digits"
2475+
$appBuild = 0
2476+
}
2477+
}
2478+
}
2479+
2480+
# Construct object to return
2481+
return [PSCustomObject]@{
2482+
MajorMinorVersion = $majorMinorVersion
2483+
BuildNumber = $appBuild
2484+
RevisionNumber = $appRevision
2485+
}
2486+
}

Actions/CompileApps/Compile.ps1

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
Param(
2+
[Parameter(HelpMessage = "The GitHub token running the action", Mandatory = $false)]
3+
[string] $token,
4+
[Parameter(HelpMessage = "ArtifactUrl to use for the build", Mandatory = $false)]
5+
[string] $artifact = "",
6+
[Parameter(HelpMessage = "Project folder", Mandatory = $false)]
7+
[string] $project = "",
8+
[Parameter(HelpMessage = "Specifies a mode to use for the build steps", Mandatory = $false)]
9+
[string] $buildMode = 'Default',
10+
[Parameter(HelpMessage = "A path to a JSON-formatted list of dependency apps", Mandatory = $false)]
11+
[string] $dependencyAppsJson = '',
12+
[Parameter(HelpMessage = "A path to a JSON-formatted list of dependency test apps", Mandatory = $false)]
13+
[string] $dependencyTestAppsJson = '',
14+
[Parameter(HelpMessage = "RunId of the baseline workflow run", Mandatory = $false)]
15+
[string] $baselineWorkflowRunId = '0',
16+
[Parameter(HelpMessage = "SHA of the baseline workflow run", Mandatory = $false)]
17+
[string] $baselineWorkflowSHA = ''
18+
)
19+
20+
. (Join-Path -Path $PSScriptRoot -ChildPath "..\AL-Go-Helper.ps1" -Resolve)
21+
Import-Module (Join-Path -Path $PSScriptRoot "..\.Modules\CompileFromWorkspace.psm1" -Resolve)
22+
Import-Module (Join-Path $PSScriptRoot '..\TelemetryHelper.psm1' -Resolve)
23+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "..\DetermineProjectsToBuild\DetermineProjectsToBuild.psm1" -Resolve) -DisableNameChecking
24+
DownloadAndImportBcContainerHelper
25+
26+
# ANALYZE - Analyze the repository and determine settings
27+
$baseFolder = (Get-BasePath)
28+
$settings = $env:Settings | ConvertFrom-Json | ConvertTo-HashTable
29+
$settings = AnalyzeRepo -settings $settings -baseFolder $baseFolder -project $project -doNotCheckArtifactSetting
30+
$settings = CheckAppDependencyProbingPaths -settings $settings -token $token -baseFolder $baseFolder -project $project
31+
32+
# Check if there are any app folders or test app folders to compile
33+
if ($settings.appFolders.Count -eq 0 -and $settings.testFolders.Count -eq 0) {
34+
Write-Host "No app folders or test app folders specified for compilation. Skipping compilation step."
35+
return
36+
}
37+
38+
$projectFolder = Join-Path $baseFolder $project
39+
Push-Location $projectFolder
40+
try {
41+
# Set up output folders
42+
$buildArtifactFolder = Join-Path $projectFolder ".buildartifacts"
43+
$appOutputFolder = Join-Path $buildArtifactFolder "Apps"
44+
$testAppOutputFolder = Join-Path $buildArtifactFolder "TestApps"
45+
if (-not (Test-Path $buildArtifactFolder)) {
46+
New-Item $buildArtifactFolder -ItemType Directory -Force | Out-Null
47+
}
48+
if (-not (Test-Path $appOutputFolder)) {
49+
New-Item $appOutputFolder -ItemType Directory -Force | Out-Null
50+
}
51+
if (-not (Test-Path $testAppOutputFolder)) {
52+
New-Item $testAppOutputFolder -ItemType Directory -Force | Out-Null
53+
}
54+
55+
# Check for precompile and postcompile overrides
56+
$scriptOverrides = Get-ScriptOverrides -ALGoFolderName (Join-Path $projectFolder ".AL-Go") -OverrideScriptNames @("PreCompileApp", "PostCompileApp")
57+
58+
# Prepare build metadata
59+
$buildMetadata = Get-BuildMetadata
60+
61+
# Get version number
62+
$versionNumber = Get-VersionNumber -Settings $settings
63+
64+
# Get ruleset file if specified
65+
$rulesetPath = $settings.rulesetFile
66+
if ($settings.rulesetFile) {
67+
$rulesetPath = Join-Path $projectFolder $settings.rulesetFile -Resolve
68+
if (-not (Test-Path $rulesetPath)) {
69+
throw "Ruleset file specified in settings.rulesetFile not found at path '$rulesetPath'."
70+
}
71+
}
72+
73+
# Read existing install apps and test apps from JSON files
74+
$dependencyApps = @()
75+
$dependencyTestApps = @()
76+
77+
if ($dependencyAppsJson -and (Test-Path $dependencyAppsJson)) {
78+
try {
79+
$dependencyApps += Get-Content -Path $dependencyAppsJson | ConvertFrom-Json
80+
}
81+
catch {
82+
throw "Failed to parse JSON file at path '$dependencyAppsJson'. Error: $($_.Exception.Message)"
83+
}
84+
}
85+
86+
if ($dependencyTestAppsJson -and (Test-Path $dependencyTestAppsJson)) {
87+
try {
88+
$dependencyTestApps += Get-Content -Path $dependencyTestAppsJson | ConvertFrom-Json
89+
}
90+
catch {
91+
throw "Failed to parse JSON file at path '$dependencyTestAppsJson'. Error: $($_.Exception.Message)"
92+
}
93+
}
94+
95+
# Set up a compiler folder
96+
$containerName = GetContainerName($project)
97+
$cacheFolder = ""
98+
if ($settings.gitHubRunner -like "windows-*" -or $settings.gitHubRunner -like "ubuntu-*") {
99+
# On GitHub-hosted runners, use a folder in the runner temp directory for caching to speed up subsequent builds
100+
$cacheFolder = Join-Path $ENV:RUNNER_TEMP ".artifactcache"
101+
}
102+
$compilerFolder = New-BcCompilerFolder -artifactUrl $artifact -containerName "$($containerName)compiler" -cacheFolder $cacheFolder
103+
$packageCachePath = Join-Path $compilerFolder "symbols"
104+
105+
# Copy dependency apps to the package cache so the compiler can resolve them
106+
foreach ($appFile in $dependencyApps) {
107+
$appFile = $appFile.Trim('()')
108+
if ($appFile -and (Test-Path $appFile)) {
109+
Copy-Item -Path $appFile -Destination $packageCachePath -Force
110+
OutputDebug "Copied dependency app to package cache: $(Split-Path $appFile -Leaf)"
111+
}
112+
}
113+
114+
# Incremental Builds - Determine unmodified apps from baseline workflow run if applicable
115+
if ($baselineWorkflowSHA -and $baselineWorkflowRunId -ne '0' -and $settings.incrementalBuilds.mode -eq 'modifiedApps') {
116+
#TODO: Implement support for incremental builds (AB#620492)
117+
Write-Host "Incremental builds based on modified apps is not yet implemented."
118+
}
119+
120+
if ((-not $settings.skipUpgrade) -and $settings.enableAppSourceCop) {
121+
# TODO: Missing implementation of around using latest release as a baseline (skipUpgrade) / Appsourcecop.json baseline implementation (AB#620310)
122+
Write-Host "Checking for required upgrades using AppSourceCop..."
123+
}
124+
125+
# Update the app jsons with version number (and other properties) from the app manifest files
126+
Update-AppJsonProperties -Folders ($settings.appFolders + $settings.testFolders) `
127+
-MajorMinorVersion $versionNumber.MajorMinorVersion -BuildNumber $versionNumber.BuildNumber -RevisionNumber $versionNumber.RevisionNumber `
128+
-BuildBy $buildMetadata.BuildBy -BuildUrl $buildMetadata.BuildUrl
129+
130+
# Collect common parameters for Build-AppsInWorkspace
131+
$buildParams = @{
132+
CompilerFolder = $compilerFolder
133+
PackageCachePath = $packageCachePath
134+
LogDirectory = $buildArtifactFolder
135+
Ruleset = $rulesetPath
136+
AssemblyProbingPaths = (Get-AssemblyProbingPaths -CompilerFolder $compilerFolder)
137+
PreprocessorSymbols = $settings.preprocessorSymbols
138+
Features = $settings.features
139+
MajorMinorVersion = $versionNumber.MajorMinorVersion
140+
BuildNumber = $versionNumber.BuildNumber
141+
RevisionNumber = $versionNumber.RevisionNumber
142+
MaxCpuCount = $settings.workspaceCompilation.parallelism
143+
SourceRepositoryUrl = $buildMetadata.SourceRepositoryUrl
144+
SourceCommit = $buildMetadata.SourceCommit
145+
ReportSuppressedDiagnostics = $settings.reportSuppressedDiagnostics
146+
EnableExternalRulesets = $settings.enableExternalRulesets
147+
PreCompileApp = $scriptOverrides['PreCompileApp']
148+
PostCompileApp = $scriptOverrides['PostCompileApp']
149+
Analyzers = (Get-CodeAnalyzers -Settings $settings)
150+
CustomAnalyzers = (Get-CustomAnalyzers -Settings $settings -CompilerFolder $compilerFolder)
151+
}
152+
153+
# Start compilation
154+
$appFiles = @()
155+
$testAppFiles = @()
156+
try {
157+
if ($settings.appFolders.Count -gt 0) {
158+
# Compile Apps
159+
$appFiles = Build-AppsInWorkspace @buildParams `
160+
-Folders $settings.appFolders `
161+
-OutFolder $appOutputFolder `
162+
-AppType 'app'
163+
}
164+
165+
if ($settings.testFolders.Count -gt 0) {
166+
if (-not ($settings.enableCodeAnalyzersOnTestApps)) {
167+
$buildParams.Analyzers = @()
168+
}
169+
170+
# Compile Test Apps
171+
$testAppFiles = Build-AppsInWorkspace @buildParams `
172+
-Folders $settings.testFolders `
173+
-OutFolder $testAppOutputFolder `
174+
-AppType 'testApp'
175+
}
176+
177+
} finally {
178+
New-BuildOutputFile -BuildArtifactFolder $buildArtifactFolder -BuildOutputPath (Join-Path $projectFolder "BuildOutput.txt") -DisplayInConsole -FailOn $settings.failOn
179+
}
180+
181+
# OUTPUT - Output the updated list of dependency apps and test apps to JSON files for downstream steps
182+
$dependencyApps += $appFiles
183+
$dependencyTestApps += $testAppFiles
184+
Trace-Information -message "Compilation completed. Compiled $(@($appFiles).Count) apps and $(@($testAppFiles).Count) test apps."
185+
186+
ConvertTo-Json $dependencyApps -Depth 99 -Compress | Out-File -Encoding UTF8 -FilePath $dependencyAppsJson
187+
ConvertTo-Json $dependencyTestApps -Depth 99 -Compress | Out-File -Encoding UTF8 -FilePath $dependencyTestAppsJson
188+
} finally {
189+
Pop-Location
190+
}

0 commit comments

Comments
 (0)