Skip to content

Commit b1c7cb0

Browse files
Merge branch 'feature/wrapper-module-packaging' into feat/bind-request-body-properties
2 parents 3f99034 + dec945a commit b1c7cb0

3 files changed

Lines changed: 108 additions & 47 deletions

File tree

tools/Build-WrapperModule.ps1

Lines changed: 62 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ For each module name, reproduces the pipeline the Mail spike proved:
88
99
1. kiota generate -> <out>/<Module>/src/Client (ApiClient + models)
1010
2. WrapperGenerator -> <out>/<Module>/src/Cmdlets (one *.g.cs per cmdlet)
11-
3. write csproj -> <out>/<Module>/src/
12-
4. dotnet build -> <out>/<Module>/src/bin/<Configuration>/net10.0/
13-
5. New-ModuleManifest -> <ModuleName>.psd1 next to the dll
11+
3. write client project -> <out>/<Module>/src/Client/Client.csproj
12+
4. write wrapper project -> <out>/<Module>/src/<ModuleName>.csproj
13+
5. dotnet build -> <out>/<Module>/src/bin/<Configuration>/net10.0/
14+
6. New-ModuleManifest -> <ModuleName>.psd1 next to the dll
1415
1516
Both generators consume the SAME OpenAPI document, so the wrappers always match the client
1617
they compile against.
@@ -86,26 +87,55 @@ if (-not $SpecRoot) { $SpecRoot = Join-Path $repoRoot 'openApiDocs_KiotaCompat'
8687
if (-not $OutputRoot) { $OutputRoot = Join-Path $repoRoot 'artifacts\wrapper-modules' }
8788
$generatorProject = Join-Path $repoRoot 'tools\WrapperGenerator'
8889
$authCsproj = Join-Path $repoRoot 'src\Authentication\Authentication\Microsoft.Graph.Authentication.csproj'
90+
$clientProjectTemplate = Join-Path $PSScriptRoot 'Templates\WrapperClient.csproj.template'
91+
$moduleProjectTemplate = Join-Path $PSScriptRoot 'Templates\WrapperModule.csproj.template'
8992

9093
if (-not (Get-Command kiota -ErrorAction SilentlyContinue)) {
9194
Write-Error "kiota CLI not found on PATH. Install: dotnet tool install --global Microsoft.OpenApi.Kiota"
9295
exit 1
9396
}
9497

95-
# Same extraction the parity gate uses: the emitted [Cmdlet(VerbsX.Verb, "Noun")] attribute
96-
# is the source of truth for what the dll will export, without having to load the assembly.
97-
$cmdletAttrPattern = '\[Cmdlet\(Verbs\w+\.(\w+),\s*"((?:\\.|[^"\\])*)"'
98-
function Get-EmittedCmdletNames {
99-
param([string]$CmdletsDir)
100-
Get-ChildItem -Path $CmdletsDir -Filter '*.g.cs' -File | ForEach-Object {
101-
$match = [regex]::Match((Get-Content -Path $_.FullName -Raw), $cmdletAttrPattern)
102-
if ($match.Success) {
103-
"$($match.Groups[1].Value)-$([regex]::Unescape($match.Groups[2].Value))"
104-
}
98+
function New-ProjectFromTemplate {
99+
param(
100+
[Parameter(Mandatory)][string]$TemplatePath,
101+
[Parameter(Mandatory)][string]$DestinationPath,
102+
[Parameter(Mandatory)][hashtable]$Replacements
103+
)
104+
105+
$content = Get-Content -Path $TemplatePath -Raw
106+
foreach ($placeholder in $Replacements.Keys) {
107+
$content = $content.Replace("{$placeholder}", $Replacements[$placeholder])
108+
}
109+
$unresolved = [regex]::Matches($content, '\{[A-Za-z][A-Za-z0-9]*\}') | ForEach-Object Value | Sort-Object -Unique
110+
if ($unresolved) {
111+
throw "unresolved placeholder(s) in $TemplatePath`: $($unresolved -join ', ')"
112+
}
113+
Set-Content -Path $DestinationPath -Value $content -Encoding utf8
114+
}
115+
116+
function Get-CompiledCmdletNames {
117+
param([Parameter(Mandatory)][string]$AssemblyPath)
118+
119+
# Import in a child process so discovery observes the compiled binary PowerShell will load,
120+
# and so assemblies from one module cannot contaminate or lock the next module's build.
121+
$escapedAssemblyPath = $AssemblyPath.Replace("'", "''")
122+
$discovery = @"
123+
`$ErrorActionPreference = 'Stop'
124+
`$module = Import-Module -Name '$escapedAssemblyPath' -PassThru
125+
[pscustomobject]@{ Cmdlets = @(`$module.ExportedCmdlets.Keys | Sort-Object) } |
126+
ConvertTo-Json -Compress
127+
"@
128+
$encodedDiscovery = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($discovery))
129+
$output = & pwsh -NoProfile -NonInteractive -EncodedCommand $encodedDiscovery 2>&1
130+
if ($LASTEXITCODE -ne 0) {
131+
throw "compiled module discovery failed: $(($output | Select-Object -Last 3) -join ' | ')"
105132
}
133+
$json = $output | Where-Object { $_ -match '^\{' } | Select-Object -Last 1
134+
if (-not $json) { throw 'compiled module discovery produced no result' }
135+
@((ConvertFrom-Json $json).Cmdlets)
106136
}
107137

108-
function Build-OneModule {
138+
function Build-Module {
109139
param([string]$Name)
110140

111141
$started = Get-Date
@@ -177,38 +207,22 @@ function Build-OneModule {
177207
return $result
178208
}
179209

180-
# Relative to $srcDir rather than the absolute $authCsproj, so the csproj is portable
181-
# across clones and stays correct if a module's output folder ever moves (the eventual
182-
# src/<Module>/<ApiVersion>/ commit target sits at a different depth than
183-
# artifacts/wrapper-modules/<Module>/src/).
210+
$clientAssemblyName = "$moduleName.Client"
211+
$clientCsprojPath = Join-Path $clientDir 'Client.csproj'
212+
New-ProjectFromTemplate -TemplatePath $clientProjectTemplate -DestinationPath $clientCsprojPath -Replacements @{
213+
ClientAssemblyName = $clientAssemblyName
214+
}
215+
216+
# Project references are relative so generated projects remain portable across clones
217+
# and across the artifacts and eventual src/<Module>/<ApiVersion>/wrapper layouts.
184218
$csprojPath = Join-Path $srcDir "$moduleName.csproj"
185219
$authCsprojRelative = [System.IO.Path]::GetRelativePath($srcDir, $authCsproj) -replace '/', '\'
186-
@"
187-
<!-- Generated by tools/Build-WrapperModule.ps1. Regenerate rather than edit by hand. -->
188-
<Project Sdk="Microsoft.NET.Sdk">
189-
190-
<PropertyGroup>
191-
<TargetFramework>net10.0</TargetFramework>
192-
<LangVersion>latest</LangVersion>
193-
<Nullable>enable</Nullable>
194-
<ImplicitUsings>enable</ImplicitUsings>
195-
<AssemblyName>$moduleName</AssemblyName>
196-
<!-- Copy every dependency next to the module dll so Import-Module resolves them. -->
197-
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
198-
<NoWarn>`$(NoWarn);CS1591</NoWarn>
199-
</PropertyGroup>
200-
201-
<ItemGroup>
202-
<PackageReference Include="Microsoft.Kiota.Bundle" Version="2.0.0" />
203-
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
204-
</ItemGroup>
205-
206-
<ItemGroup>
207-
<ProjectReference Include="$authCsprojRelative" />
208-
</ItemGroup>
209-
210-
</Project>
211-
"@ | Set-Content -Path $csprojPath -Encoding utf8
220+
$clientCsprojRelative = [System.IO.Path]::GetRelativePath($srcDir, $clientCsprojPath) -replace '/', '\'
221+
New-ProjectFromTemplate -TemplatePath $moduleProjectTemplate -DestinationPath $csprojPath -Replacements @{
222+
ModuleAssemblyName = $moduleName
223+
ClientProjectPath = $clientCsprojRelative
224+
AuthenticationProjectPath = $authCsprojRelative
225+
}
212226

213227
$buildOut = & dotnet build $csprojPath -c $Configuration --nologo -v minimal 2>&1
214228
if ($LASTEXITCODE -ne 0) {
@@ -217,10 +231,11 @@ function Build-OneModule {
217231
return $result
218232
}
219233

220-
$cmdlets = @(Get-EmittedCmdletNames -CmdletsDir $cmdletsDir)
234+
$binDir = Join-Path $srcDir "bin\$Configuration\net10.0"
235+
$assemblyPath = Join-Path $binDir "$moduleName.dll"
236+
$cmdlets = @(Get-CompiledCmdletNames -AssemblyPath $assemblyPath)
221237
if ($cmdlets.Count -eq 0) { $result.FailedAt = 'manifest'; $result.Error = 'no cmdlets emitted'; return $result }
222238

223-
$binDir = Join-Path $srcDir "bin\$Configuration\net10.0"
224239
$psd1Path = Join-Path $binDir "$moduleName.psd1"
225240
New-ModuleManifest -Path $psd1Path `
226241
-RootModule "$moduleName.dll" `
@@ -247,7 +262,7 @@ function Build-OneModule {
247262

248263
$results = foreach ($name in $Module) {
249264
Write-Host "=== $name ===" -ForegroundColor Cyan
250-
$r = Build-OneModule -Name $name
265+
$r = Build-Module -Name $name
251266
if ($r.Status -eq 'OK') {
252267
Write-Host " OK: $($r.CmdletCount) cmdlets -> $($r.Psd1) ($($r.Seconds)s)" -ForegroundColor Green
253268
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!-- Generated by tools/Build-WrapperModule.ps1. Regenerate rather than edit by hand. -->
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
4+
<PropertyGroup>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<LangVersion>latest</LangVersion>
7+
<Nullable>enable</Nullable>
8+
<ImplicitUsings>enable</ImplicitUsings>
9+
<AssemblyName>{ClientAssemblyName}</AssemblyName>
10+
<NoWarn>$(NoWarn);CS1591</NoWarn>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="Microsoft.Kiota.Bundle" Version="2.0.0" />
15+
</ItemGroup>
16+
17+
</Project>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!-- Generated by tools/Build-WrapperModule.ps1. Regenerate rather than edit by hand. -->
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
4+
<PropertyGroup>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<LangVersion>latest</LangVersion>
7+
<Nullable>enable</Nullable>
8+
<ImplicitUsings>enable</ImplicitUsings>
9+
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
10+
<AssemblyName>{ModuleAssemblyName}</AssemblyName>
11+
<!-- Copy every dependency next to the module dll so Import-Module resolves them. -->
12+
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
13+
<NoWarn>$(NoWarn);CS1591</NoWarn>
14+
</PropertyGroup>
15+
16+
<ItemGroup>
17+
<Compile Include="Cmdlets\**\*.g.cs" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<ProjectReference Include="{ClientProjectPath}" />
26+
<ProjectReference Include="{AuthenticationProjectPath}" />
27+
</ItemGroup>
28+
29+
</Project>

0 commit comments

Comments
 (0)