diff --git a/.github/workflows/check-upstream.yml b/.github/workflows/check-upstream.yml index b554284..0c1aacf 100644 --- a/.github/workflows/check-upstream.yml +++ b/.github/workflows/check-upstream.yml @@ -96,7 +96,10 @@ jobs: run: | Import-Module ./.hub/module/LANCommander.Redistributables -Force - $payload = Resolve-RedistributablePayload -RepositoryPath . -StagingPath ./.payload + # -RefreshReference is a no-op unless Source.Mode is 'none', where it + # lets source.ps1 refresh the committed Reference/ config the schema is + # generated from. It never yields a payload. + $payload = Resolve-RedistributablePayload -RepositoryPath . -StagingPath ./.payload -RefreshReference $schema = Update-OptionSchemaFile -RepositoryPath . -PayloadPath $payload.PayloadPath # LastKnownVersion is what the next run compares against, so it has to @@ -143,6 +146,7 @@ jobs: OptionSchema.yml redistributable.yml Files/** + Reference/** body: | Upstream moved from `${{ steps.upstream.outputs.previous }}` to `${{ steps.upstream.outputs.version }}`. diff --git a/.gitmodules b/.gitmodules index 46f1272..c2fe553 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "LANCommander.Redistributables.UmuLauncher"] path = LANCommander.Redistributables.UmuLauncher url = https://github.com/LANCommander/LANCommander.Redistributables.UmuLauncher.git +[submodule "LANCommander.Redistributables.dgVoodoo2"] + path = LANCommander.Redistributables.dgVoodoo2 + url = https://github.com/LANCommander/LANCommander.Redistributables.dgVoodoo2.git diff --git a/LANCommander.Redistributables.dgVoodoo2 b/LANCommander.Redistributables.dgVoodoo2 new file mode 160000 index 0000000..dfb390f --- /dev/null +++ b/LANCommander.Redistributables.dgVoodoo2 @@ -0,0 +1 @@ +Subproject commit dfb390f4b551506892385701f22fcdbeea3121fc diff --git a/module/LANCommander.Redistributables/Private/Parsers/Ini.ps1 b/module/LANCommander.Redistributables/Private/Parsers/Ini.ps1 index 2459658..18c12b4 100644 --- a/module/LANCommander.Redistributables/Private/Parsers/Ini.ps1 +++ b/module/LANCommander.Redistributables/Private/Parsers/Ini.ps1 @@ -9,10 +9,15 @@ Section headers become group nodes; keys outside any section land at the root. ChoiceCommentPattern lets a redistributable harvest valid values out of the - comments that sit above a key -- a very common convention (dgVoodoo.conf - documents its allowed values this way). The pattern must expose a named group - 'choices'; the captured text is split on commas or pipes. This keeps odd - conventions declarative instead of requiring a bespoke parser. + comments that sit directly above a key -- a very common convention. The pattern + must expose a named group 'choices'; the captured text is split on commas or + pipes. This keeps odd conventions declarative instead of requiring a bespoke + parser. + + It only reaches comments that survive the blank-line rule below, so it cannot + read a config that documents a whole section in one block above a blank line. + dgVoodoo.conf is the notable example, and it uses ConfigFormat 'custom' with a + repository-local Parse-Config.ps1 instead. #> function ConvertFrom-IniConfig { [CmdletBinding()] diff --git a/module/LANCommander.Redistributables/Public/Resolve-RedistributablePayload.ps1 b/module/LANCommander.Redistributables/Public/Resolve-RedistributablePayload.ps1 index a33ff33..35cd5bc 100644 --- a/module/LANCommander.Redistributables/Public/Resolve-RedistributablePayload.ps1 +++ b/module/LANCommander.Redistributables/Public/Resolve-RedistributablePayload.ps1 @@ -9,9 +9,14 @@ upstream release into a staging directory and reports the version. vendored Uses the committed Files/ directory. The version comes from LastKnownVersion in redistributable.yml. - none No payload at all. The package ships scripts only and the Install - script fetches from the vendor on the client. This is the mode used - when the upstream license does not permit redistribution. + none No payload at all. The package ships scripts only and the files are + fetched from the vendor after import. This is the mode used when the + upstream license does not permit redistribution. + + source.ps1 is still useful here: it tracks the upstream version, and + with -RefreshReference it refreshes the committed Reference/ config + the option schema is generated from. Neither path ever returns a + PayloadPath, so nothing can be bundled by accident. source.ps1 contract: -CheckOnly write the version to stdout and exit @@ -23,6 +28,14 @@ Where a download-mode payload should be extracted. Defaults to a temp directory. .PARAMETER CheckOnly Resolve the version without downloading anything. +.PARAMETER RefreshReference + Only meaningful in 'none' mode. Lets source.ps1 refresh the repository's + committed Reference/ directory -- the config a redistributable that ships no + payload still needs in order to generate an option schema. + + This is how the scheduled upstream check picks up options added upstream. It + is deliberately opt-in and Invoke-RedistributableBuild never passes it, so a + build can never acquire files it is not allowed to publish. .OUTPUTS An object with Version, Changelog, PayloadPath and Mode. #> @@ -31,7 +44,8 @@ function Resolve-RedistributablePayload { param( [Parameter(Mandatory)][string] $RepositoryPath, [string] $StagingPath, - [switch] $CheckOnly + [switch] $CheckOnly, + [switch] $RefreshReference ) $definition = Get-RedistributableDefinition -Path $RepositoryPath @@ -117,9 +131,67 @@ function Resolve-RedistributablePayload { } 'none' { + $hasSource = Test-Path -LiteralPath $sourceScript + + # PayloadPath stays null in every branch below. That is the guarantee + # this mode exists to make: when the upstream license forbids + # redistribution there must be no way for a build to end up with + # bundleable files, however it was invoked. + + if ($CheckOnly -and $hasSource) { + # Mirror 'download'. LastKnownVersion is what a *build* pins to, so + # letting it answer here too would make the scheduled upstream check + # compare a value against itself and never see upstream move. + $version = [string] (& $sourceScript -CheckOnly | Select-Object -Last 1) + + if ([string]::IsNullOrWhiteSpace($version)) { + throw 'source.ps1 -CheckOnly produced no version' + } + + return [pscustomobject] @{ + Version = $version.Trim() + Changelog = $null + PayloadPath = $null + Mode = $mode + } + } + + if ($RefreshReference -and $hasSource) { + # source.ps1 writes the reference config straight into the + # repository, because unlike a payload it is a committed artefact: + # it is the only thing the option schema can be generated from, and + # the upstream-update pull request has to carry it. + $reference = Join-Path $RepositoryPath 'Reference' + + if (-not (Test-Path -LiteralPath $reference)) { + $null = New-Item -ItemType Directory -Path $reference -Force + } + + $output = & $sourceScript -OutputPath $reference + + $json = @($output) | Where-Object { $_ -is [string] -and $_.TrimStart().StartsWith('{') } | Select-Object -Last 1 + + if (-not $json) { + throw 'source.ps1 did not emit a JSON result object with a Version' + } + + $result = $json | ConvertFrom-Json + + if ([string]::IsNullOrWhiteSpace($result.Version)) { + throw 'source.ps1 emitted a result with no Version' + } + + return [pscustomobject] @{ + Version = ([string] $result.Version).Trim() + Changelog = $result.Changelog + PayloadPath = $null + Mode = $mode + } + } + $version = [string] $definition['LastKnownVersion'] - if ([string]::IsNullOrWhiteSpace($version) -and (Test-Path -LiteralPath $sourceScript)) { + if ([string]::IsNullOrWhiteSpace($version) -and $hasSource) { # Even with nothing to bundle, source.ps1 can still track the # upstream version so releases stay aligned with it. $version = [string] (& $sourceScript -CheckOnly | Select-Object -Last 1) diff --git a/template/.github/workflows/build.yml b/template/.github/workflows/build.yml index fb9cd62..a4c466b 100644 --- a/template/.github/workflows/build.yml +++ b/template/.github/workflows/build.yml @@ -16,6 +16,7 @@ on: - 'OptionSchema.yml' - 'Scripts/**' - 'Files/**' + - 'Reference/**' - 'source.ps1' - 'Parse-Config.ps1' pull_request: @@ -25,6 +26,7 @@ on: - 'OptionSchema.yml' - 'Scripts/**' - 'Files/**' + - 'Reference/**' - 'source.ps1' - 'Parse-Config.ps1' workflow_dispatch: diff --git a/template/redistributable.yml b/template/redistributable.yml index 2dc693d..813e9c7 100644 --- a/template/redistributable.yml +++ b/template/redistributable.yml @@ -30,9 +30,16 @@ LastKnownVersion: "0.0.0" Source: # download source.ps1 fetches the upstream release at build time. # vendored the payload is committed under Files/. - # none no payload is shipped; the Install script downloads on the client. - # This is the mode to use when the upstream license does not permit - # redistribution -- the SDK runs Install even with no archives. + # none no payload is shipped; the files are fetched from the vendor after + # import, by the Install script on the client or by the Package script + # on the server. This is the mode to use when the upstream license does + # not permit redistribution -- the SDK runs Install even with no + # archives. + # + # source.ps1 is still worth writing in this mode. It reports the + # upstream version so the scheduled check still fires, and it populates + # Reference/ (see ConfigPaths below). It is never allowed to yield a + # payload here, so keep it to the config file and the license text. Mode: download # Consumed by source.ps1. github-release and html both have helpers in the @@ -47,6 +54,13 @@ Source: # so an option added upstream appears automatically on the next scheduled run -- # no script change required. # +# With Source.Mode 'none' there is no payload to resolve against, so the config has +# to be committed. The convention is Reference/, which the scheduled upstream check +# refreshes from source.ps1 and includes in its pull request: +# +# ConfigPaths: +# - Reference/upstream.conf +# # Remove this section entirely for a plain runtime installer with no options. ConfigPaths: [] @@ -55,8 +69,11 @@ ConfigPaths: [] # 'custom' requires a Parse-Config.ps1 in this repository. # ConfigFormat: ini -# Optional. Harvests valid values out of the comment above each key, for configs -# that document their options inline. Must expose a named group 'choices'. +# Optional. Harvests valid values out of the comment directly above each key, for +# configs that document their options inline. Must expose a named group 'choices'. +# +# A blank line ends a comment block, so this cannot read a config that documents a +# whole section in one block above a blank line. Write a Parse-Config.ps1 for that. # ChoiceCommentPattern: '(?i)(?:values?|options?)\s*:\s*(?.+)' # Scripts ----------------------------------------------------------------------