@@ -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
1516Both generators consume the SAME OpenAPI document, so the wrappers always match the client
1617they compile against.
@@ -86,26 +87,55 @@ if (-not $SpecRoot) { $SpecRoot = Join-Path $repoRoot 'openApiDocs_KiotaCompat'
8687if (-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
9093if (-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 }
0 commit comments