@@ -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.
@@ -76,26 +77,55 @@ if (-not $SpecRoot) { $SpecRoot = Join-Path $repoRoot 'openApiDocs_KiotaCompat'
7677if (-not $OutputRoot ) { $OutputRoot = Join-Path $repoRoot ' artifacts\wrapper-modules' }
7778$generatorProject = Join-Path $repoRoot ' tools\WrapperGenerator'
7879$authCsproj = Join-Path $repoRoot ' src\Authentication\Authentication\Microsoft.Graph.Authentication.csproj'
80+ $clientProjectTemplate = Join-Path $PSScriptRoot ' Templates\WrapperClient.csproj.template'
81+ $moduleProjectTemplate = Join-Path $PSScriptRoot ' Templates\WrapperModule.csproj.template'
7982
8083if (-not (Get-Command kiota - ErrorAction SilentlyContinue)) {
8184 Write-Error " kiota CLI not found on PATH. Install: dotnet tool install --global Microsoft.OpenApi.Kiota"
8285 exit 1
8386}
8487
85- # Same extraction the parity gate uses: the emitted [Cmdlet(VerbsX.Verb, "Noun")] attribute
86- # is the source of truth for what the dll will export, without having to load the assembly.
87- $cmdletAttrPattern = ' \[Cmdlet\(Verbs\w+\.(\w+),\s*"((?:\\.|[^"\\])*)"'
88- function Get-EmittedCmdletNames {
89- param ([string ]$CmdletsDir )
90- Get-ChildItem - Path $CmdletsDir - Filter ' *.g.cs' - File | ForEach-Object {
91- $match = [regex ]::Match((Get-Content - Path $_.FullName - Raw), $cmdletAttrPattern )
92- if ($match.Success ) {
93- " $ ( $match.Groups [1 ].Value) -$ ( [regex ]::Unescape($match.Groups [2 ].Value)) "
94- }
88+ function New-ProjectFromTemplate {
89+ param (
90+ [Parameter (Mandatory )][string ]$TemplatePath ,
91+ [Parameter (Mandatory )][string ]$DestinationPath ,
92+ [Parameter (Mandatory )][hashtable ]$Replacements
93+ )
94+
95+ $content = Get-Content - Path $TemplatePath - Raw
96+ foreach ($placeholder in $Replacements.Keys ) {
97+ $content = $content.Replace (" {$placeholder }" , $Replacements [$placeholder ])
98+ }
99+ $unresolved = [regex ]::Matches($content , ' \{[A-Za-z][A-Za-z0-9]*\}' ) | ForEach-Object Value | Sort-Object - Unique
100+ if ($unresolved ) {
101+ throw " unresolved placeholder(s) in $TemplatePath `: $ ( $unresolved -join ' , ' ) "
102+ }
103+ Set-Content - Path $DestinationPath - Value $content - Encoding utf8
104+ }
105+
106+ function Get-CompiledCmdletNames {
107+ param ([Parameter (Mandatory )][string ]$AssemblyPath )
108+
109+ # Import in a child process so discovery observes the compiled binary PowerShell will load,
110+ # and so assemblies from one module cannot contaminate or lock the next module's build.
111+ $escapedAssemblyPath = $AssemblyPath.Replace (" '" , " ''" )
112+ $discovery = @"
113+ `$ ErrorActionPreference = 'Stop'
114+ `$ module = Import-Module -Name '$escapedAssemblyPath ' -PassThru
115+ [pscustomobject]@{ Cmdlets = @(`$ module.ExportedCmdlets.Keys | Sort-Object) } |
116+ ConvertTo-Json -Compress
117+ "@
118+ $encodedDiscovery = [Convert ]::ToBase64String([Text.Encoding ]::Unicode.GetBytes($discovery ))
119+ $output = & pwsh - NoProfile - NonInteractive - EncodedCommand $encodedDiscovery 2>&1
120+ if ($LASTEXITCODE -ne 0 ) {
121+ throw " compiled module discovery failed: $ ( ($output | Select-Object - Last 3 ) -join ' | ' ) "
95122 }
123+ $json = $output | Where-Object { $_ -match ' ^\{' } | Select-Object - Last 1
124+ if (-not $json ) { throw ' compiled module discovery produced no result' }
125+ @ ((ConvertFrom-Json $json ).Cmdlets)
96126}
97127
98- function Build-OneModule {
128+ function Build-Module {
99129 param ([string ]$Name )
100130
101131 $started = Get-Date
@@ -157,38 +187,22 @@ function Build-OneModule {
157187 return $result
158188 }
159189
160- # Relative to $srcDir rather than the absolute $authCsproj, so the csproj is portable
161- # across clones and stays correct if a module's output folder ever moves (the eventual
162- # src/<Module>/<ApiVersion>/ commit target sits at a different depth than
163- # artifacts/wrapper-modules/<Module>/src/).
190+ $clientAssemblyName = " $moduleName .Client"
191+ $clientCsprojPath = Join-Path $clientDir ' Client.csproj'
192+ New-ProjectFromTemplate - TemplatePath $clientProjectTemplate - DestinationPath $clientCsprojPath - Replacements @ {
193+ ClientAssemblyName = $clientAssemblyName
194+ }
195+
196+ # Project references are relative so generated projects remain portable across clones
197+ # and across the artifacts and eventual src/<Module>/<ApiVersion>/wrapper layouts.
164198 $csprojPath = Join-Path $srcDir " $moduleName .csproj"
165199 $authCsprojRelative = [System.IO.Path ]::GetRelativePath($srcDir , $authCsproj ) -replace ' /' , ' \'
166- @"
167- <!-- Generated by tools/Build-WrapperModule.ps1 - do not edit or commit. -->
168- <Project Sdk="Microsoft.NET.Sdk">
169-
170- <PropertyGroup>
171- <TargetFramework>net10.0</TargetFramework>
172- <LangVersion>latest</LangVersion>
173- <Nullable>enable</Nullable>
174- <ImplicitUsings>enable</ImplicitUsings>
175- <AssemblyName>$moduleName </AssemblyName>
176- <!-- Copy every dependency next to the module dll so Import-Module resolves them. -->
177- <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
178- <NoWarn>`$ (NoWarn);CS1591</NoWarn>
179- </PropertyGroup>
180-
181- <ItemGroup>
182- <PackageReference Include="Microsoft.Kiota.Bundle" Version="2.0.0" />
183- <PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
184- </ItemGroup>
185-
186- <ItemGroup>
187- <ProjectReference Include="$authCsprojRelative " />
188- </ItemGroup>
189-
190- </Project>
191- "@ | Set-Content - Path $csprojPath - Encoding utf8
200+ $clientCsprojRelative = [System.IO.Path ]::GetRelativePath($srcDir , $clientCsprojPath ) -replace ' /' , ' \'
201+ New-ProjectFromTemplate - TemplatePath $moduleProjectTemplate - DestinationPath $csprojPath - Replacements @ {
202+ ModuleAssemblyName = $moduleName
203+ ClientProjectPath = $clientCsprojRelative
204+ AuthenticationProjectPath = $authCsprojRelative
205+ }
192206
193207 $buildOut = & dotnet build $csprojPath - c $Configuration -- nologo - v minimal 2>&1
194208 if ($LASTEXITCODE -ne 0 ) {
@@ -197,10 +211,11 @@ function Build-OneModule {
197211 return $result
198212 }
199213
200- $cmdlets = @ (Get-EmittedCmdletNames - CmdletsDir $cmdletsDir )
214+ $binDir = Join-Path $srcDir " bin\$Configuration \net10.0"
215+ $assemblyPath = Join-Path $binDir " $moduleName .dll"
216+ $cmdlets = @ (Get-CompiledCmdletNames - AssemblyPath $assemblyPath )
201217 if ($cmdlets.Count -eq 0 ) { $result.FailedAt = ' manifest' ; $result.Error = ' no cmdlets emitted' ; return $result }
202218
203- $binDir = Join-Path $srcDir " bin\$Configuration \net10.0"
204219 $psd1Path = Join-Path $binDir " $moduleName .psd1"
205220 New-ModuleManifest - Path $psd1Path `
206221 - RootModule " $moduleName .dll" `
@@ -227,7 +242,7 @@ function Build-OneModule {
227242
228243$results = foreach ($name in $Module ) {
229244 Write-Host " === $name ===" - ForegroundColor Cyan
230- $r = Build-OneModule - Name $name
245+ $r = Build-Module - Name $name
231246 if ($r.Status -eq ' OK' ) {
232247 Write-Host " OK: $ ( $r.CmdletCount ) cmdlets -> $ ( $r.Psd1 ) ($ ( $r.Seconds ) s)" - ForegroundColor Green
233248 }
0 commit comments