From 01f5da59ded1246e6bbf9acb4decee88d1f59f94 Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 26 Aug 2026 15:09:00 -0400 Subject: [PATCH 1/7] fix: restore truncated workload-install scripts and guard against regression Both installers were committed truncated mid-statement and NUL-padded, so the publicly curl'd installer could never complete. workload-install.sh ended at for DOTNET_SDK in $INSTALLED_DOTNET_SD followed by 134 NUL bytes, and workload-install.ps1 ended inside its catch block at `Write-Host `. The final lines are restored from the intact copies on main. validate-version-map.yml did not catch this because Generate-InstallScripts.ps1 only compares the auto-generated version-map block, and that block was untouched, so it reported "OK" for a file missing its tail. The generator now also verifies each installer contains no NUL bytes and ends with its expected final statement, and fails otherwise. Also restores the SDK enumeration fix that was lost in the same regression. --update-all-workloads matched only `^6|^7`, silently ignoring every installed .NET 8/9/10/11 SDK. It now matches majors 6-9 and any two-or-more digit major, so future majors need no further edit. Finally, extracts the SDK-version to feature-band computation into compute_target_version_band() delimited by BEGIN/END VERSION BAND DETECTION markers. Behaviour is unchanged - MANIFEST_NAME was already equal to the plain band in the fall-through case - but the logic is now testable in isolation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- workload/scripts/Generate-InstallScripts.ps1 | 34 ++++++++++++ workload/scripts/workload-install.ps1 | 11 +++- workload/scripts/workload-install.sh | 54 ++++++++++++++------ 3 files changed, 81 insertions(+), 18 deletions(-) diff --git a/workload/scripts/Generate-InstallScripts.ps1 b/workload/scripts/Generate-InstallScripts.ps1 index 390d37c7a..ca35c2330 100644 --- a/workload/scripts/Generate-InstallScripts.ps1 +++ b/workload/scripts/Generate-InstallScripts.ps1 @@ -137,6 +137,30 @@ Please add these markers around the existing LatestVersionMap block, then rerun. return $replaced } +function Test-ScriptIntegrity { + <# + Guards against the file-truncation / NUL-padding corruption that silently landed + in workload-install.sh (see git history around the version-map SSOT change). + The version-map drift check alone cannot catch it: it only compares the + auto-generated block, so a script whose *tail* is missing still reports "OK". + #> + param([string]$Path, [string]$ExpectedTail) + + $bytes = [System.IO.File]::ReadAllBytes($Path) + if ($bytes -contains 0) { + Write-Host " CORRUPT: $Path contains NUL bytes." -ForegroundColor Red + return $false + } + $text = [System.Text.Encoding]::UTF8.GetString($bytes) + if ($text.TrimEnd() -notmatch ([regex]::Escape($ExpectedTail) + '\s*$')) { + Write-Host " TRUNCATED: $Path does not end with '$ExpectedTail'." -ForegroundColor Red + Write-Host (" actual tail: " + ($text.TrimEnd() -split "`r?`n" | Select-Object -Last 1)) + return $false + } + Write-Host " OK: $Path integrity (no NULs, expected tail)." -ForegroundColor Green + return $true +} + function Detect-LineEnding { param([string]$Path) $bytes = [System.IO.File]::ReadAllBytes($Path) @@ -197,6 +221,16 @@ $ps1New = Replace-Block -FilePath $Ps1Path -NewBlock $ps1Block -LineEnding $p $okSh = Write-Or-Check -Path $ShPath -NewContent $shNew -CheckOnly:$Check $okPs1 = Write-Or-Check -Path $Ps1Path -NewContent $ps1New -CheckOnly:$Check +$intactSh = Test-ScriptIntegrity -Path $ShPath -ExpectedTail 'echo "DONE"' +$intactPs1 = Test-ScriptIntegrity -Path $Ps1Path -ExpectedTail 'Write-Host "`nDone"' + +if (-not $intactSh -or -not $intactPs1) { + Write-Host "" + Write-Host "An install script is corrupt (truncated or NUL-padded)." -ForegroundColor Red + Write-Host "Restore it from git history before regenerating the version map." + exit 1 +} + if ($Check -and (-not $okSh -or -not $okPs1)) { Write-Host "" Write-Host "version-map.json and the install scripts are out of sync." -ForegroundColor Red diff --git a/workload/scripts/workload-install.ps1 b/workload/scripts/workload-install.ps1 index 276b790a3..feccd60f7 100644 --- a/workload/scripts/workload-install.ps1 +++ b/workload/scripts/workload-install.ps1 @@ -304,7 +304,7 @@ if (Get-Command $DotnetCommand -ErrorAction SilentlyContinue) { if ($UpdateAllWorkloads.IsPresent) { - $InstalledDotnetSdks = Invoke-Expression "& '$DotnetCommand' --list-sdks | Select-String -Pattern '^6|^7'" | ForEach-Object {$_ -replace (" \[.*","")} + $InstalledDotnetSdks = Invoke-Expression "& '$DotnetCommand' --list-sdks | Select-String -Pattern '^([6-9]|[1-9][0-9]+)\.'" | ForEach-Object {$_ -replace (" \[.*","")} } else { @@ -329,4 +329,11 @@ else Install-TizenWorkload -DotnetVersion $DotnetSdk } catch { - Write-Host \ No newline at end of file + Write-Host "Failed to install Tizen Workload for sdk $DotnetSdk" + Write-Host "$_" + Continue + } + } +} + +Write-Host "`nDone" diff --git a/workload/scripts/workload-install.sh b/workload/scripts/workload-install.sh index c863f72f2..ddd8d6a3f 100755 --- a/workload/scripts/workload-install.sh +++ b/workload/scripts/workload-install.sh @@ -165,10 +165,37 @@ if [ ! -x "$DOTNET_COMMAND" ]; then exit 1 fi +# BEGIN VERSION BAND DETECTION -- covered by scripts/test-version-band.sh +# Map a full .NET SDK version to the SDK feature band used for the workload +# manifest directory / NuGet package suffix. +# 10.0.100 -> 10.0.100 +# 10.0.100-rc.2.25502.107 -> 10.0.100-rc.2 +# 11.0.100-preview.7.26381.103 -> 11.0.100-preview.7 +# 8.0.100-rtm.23512.16 -> 8.0.100-rtm +# 6.0.419 -> 6.0.400 (pre-net7 SDKs have no preview bands) +function compute_target_version_band() { + local dotnet_version="$1" + local -a array + IFS='.' read -r -a array <<< "$dotnet_version" + local current_major="${array[0]}" + local band="${array[0]}.${array[1]}.${array[2]:0:1}00" + + if [[ "$current_major" -ge "7" ]]; then + if [[ "$dotnet_version" == *"-preview"* || "$dotnet_version" == *"-rc"* || "$dotnet_version" == *"-alpha"* ]] && [[ ${#array[@]} -ge 4 ]]; then + echo "$band${array[2]:3}.${array[3]}" + return + elif [[ "$dotnet_version" == *"-rtm"* ]] && [[ ${#array[@]} -ge 3 ]]; then + echo "$band${array[2]:3}" + return + fi + fi + echo "$band" +} +# END VERSION BAND DETECTION + function install_tizenworkload() { DOTNET_VERSION=$1 IFS='.' read -r -a array <<< "$DOTNET_VERSION" - CURRENT_DOTNET_VERSION=${array[0]} DOTNET_VERSION_BAND="${array[0]}.${array[1]}.${array[2]:0:1}00" MANIFEST_NAME="$MANIFEST_BASE_NAME-$DOTNET_VERSION_BAND" @@ -180,19 +207,8 @@ function install_tizenworkload() { # Check version band if [[ "$DOTNET_TARGET_VERSION_BAND" == "" ]]; then - if [[ "$CURRENT_DOTNET_VERSION" -ge "7" ]]; then - if [[ "$DOTNET_VERSION" == *"-preview"* || $DOTNET_VERSION == *"-rc"* || $DOTNET_VERSION == *"-alpha"* ]] && [[ ${#array[@]} -ge 4 ]]; then - DOTNET_TARGET_VERSION_BAND="$DOTNET_VERSION_BAND${array[2]:3}.${array[3]}" - MANIFEST_NAME="$MANIFEST_BASE_NAME-$DOTNET_TARGET_VERSION_BAND" - elif [[ "$DOTNET_VERSION" == *"-rtm"* ]] && [[ ${#array[@]} -ge 3 ]]; then - DOTNET_TARGET_VERSION_BAND="$DOTNET_VERSION_BAND${array[2]:3}" - MANIFEST_NAME="$MANIFEST_BASE_NAME-$DOTNET_TARGET_VERSION_BAND" - else - DOTNET_TARGET_VERSION_BAND=$DOTNET_VERSION_BAND - fi - else - DOTNET_TARGET_VERSION_BAND=$DOTNET_VERSION_BAND - fi + DOTNET_TARGET_VERSION_BAND=$(compute_target_version_band "$DOTNET_VERSION") + MANIFEST_NAME="$MANIFEST_BASE_NAME-$DOTNET_TARGET_VERSION_BAND" fi # Check latest version of manifest. @@ -258,7 +274,7 @@ function install_tizenworkload() { } if [[ "$UPDATE_ALL_WORKLOADS" == "true" ]]; then - INSTALLED_DOTNET_SDKS=$($DOTNET_COMMAND --list-sdks | sed -n '/^6\|^7/p' | sed 's/ \[.*//g') + INSTALLED_DOTNET_SDKS=$($DOTNET_COMMAND --list-sdks | sed -E -n '/^([6-9]|[1-9][0-9]+)\./p' | sed 's/ \[.*//g') else INSTALLED_DOTNET_SDKS=$($DOTNET_COMMAND --version) fi @@ -266,4 +282,10 @@ fi if [ -z "$INSTALLED_DOTNET_SDKS" ]; then echo ".NET SDK version 6 or later is required to install Tizen Workload." else - for DOTNET_SDK in $INSTALLED_DOTNET_SD \ No newline at end of file + for DOTNET_SDK in $INSTALLED_DOTNET_SDKS; do + echo "Check Tizen Workload for sdk $DOTNET_SDK." + install_tizenworkload $DOTNET_SDK + done +fi + +echo "DONE" From b8473418005a83e9a97d30fd644b4fa837863de8 Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 26 Aug 2026 15:09:14 -0400 Subject: [PATCH 2/7] feat: support the .NET 11 SDK band and net11.0-tizen TFMs Adds .NET 11 support to the tizen workload alongside .NET 10. The primary target framework is net11.0-tizen11.0, which maps to TizenFX API level 15 and the already-published Samsung.Tizen.Ref.API15 targeting pack. No new reference or runtime pack is required: ref packs ship ref/net8.0 assemblies resolved by explicit paths in FrameworkList.xml, so they are independent of the consuming project's .NET version. Workload changes: * Versions.props: arcade 11.0.0-beta.26426.103 for 11.0 bands. * NuGet.config: add the dotnet11 source, and clear inherited disabledPackageSources so a developer with nuget.org disabled machine-wide does not get a confusing NU1101 for a source this file already lists. * Samsung.Tizen.Sdk.targets: add the net11.0 KnownRuntimePack. Without it the SDK cannot resolve Samsung.NETCore.App.Runtime.tizen (NETSDK1082). * RuntimeList.xml: add the .NET Runtime 11 FileList row. * template.json: offer net11.0; the default stays net10.0 because .NET 11 is still a preview SDK. * TizenApp1.csproj: Tizen.UI.Components.Material ships assets for the tizen 10.0 band only, so reference it conditionally on the resolved platform version and raise an actionable TIZENTMPL001 below that, instead of an opaque NU1101. Verification: * test-matrix.sh: add net11.0-tizen11.0 and net11.0-tizen10.0 rows. Rows whose .NET major has no installed SDK are now skipped rather than failed, so the default .NET 10 run stays green. * validate-workload-metadata.py: new checks C5/C6 tying every .NET major offered by the template or exercised by the matrix to a KnownRuntimePack and a RuntimeList.xml row. * test-version-band.sh: new test asserting the SDK version to feature band mapping (11.0.100-preview.7.26381.103 -> 11.0.100-preview.7) and that workload-install.sh and workload-install.ps1 agree. * Makefile: add validate-metadata, test-version-band and an aggregate check target that need no dotnet install. * build-matrix.yml: add a non-blocking .NET 11 preview leg. Validated by building the workload against 11.0.100-preview.7.26381.103: Samsung.NET.Sdk.Tizen.Manifest-11.0.100-preview.7 packs correctly, installs, and `dotnet new tizen --framework net11.0` builds a .tpk for both net11.0-tizen11.0 (resolving Samsung.Tizen.Ref.API15/15.0.0.19396) and net11.0-tizen10.0. version-map.json is deliberately not updated: it is a fallback cache of already-published manifest versions, so an entry for an unreleased band would make the installer download a 404. See workload/docs/net11.md for the mapping and the external blockers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-matrix.yml | 25 +++- .github/workflows/build-workload.yml | 2 + .github/workflows/validate-version-map.yml | 2 +- workload/Makefile | 15 ++ workload/NuGet.config | 8 +- workload/README.md | 15 +- workload/build/Versions.props | 10 ++ workload/docs/net11.md | 113 +++++++++++++++ workload/scripts/README.md | 28 ++++ workload/scripts/test-matrix.sh | 52 ++++++- workload/scripts/test-version-band.sh | 137 ++++++++++++++++++ .../scripts/validate-workload-metadata.py | 54 +++++++ .../data/RuntimeList.xml | 1 + .../targets/Samsung.Tizen.Sdk.targets | 8 + .../tizen/.template.config/template.json | 4 + .../tizen/TizenApp1.csproj | 21 ++- 16 files changed, 485 insertions(+), 10 deletions(-) create mode 100644 workload/docs/net11.md create mode 100755 workload/scripts/test-version-band.sh diff --git a/.github/workflows/build-matrix.yml b/.github/workflows/build-matrix.yml index 697534139..14cd73a30 100644 --- a/.github/workflows/build-matrix.yml +++ b/.github/workflows/build-matrix.yml @@ -17,6 +17,7 @@ on: branches: - main - net10.0 + - net11.0 paths: - 'workload/**' - '.github/workflows/build-matrix.yml' @@ -24,6 +25,7 @@ on: branches: - main - net10.0 + - net11.0 paths: - 'workload/**' - '.github/workflows/build-matrix.yml' @@ -51,10 +53,28 @@ jobs: - name: Run validate-workload-metadata.py run: python3 workload/scripts/validate-workload-metadata.py + - name: Run test-version-band.sh + run: bash workload/scripts/test-version-band.sh + test-matrix: - name: Multi-TFM build matrix + name: Multi-TFM build matrix (${{ matrix.name }}) needs: validate-metadata runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + include: + # Shipping band. Blocking. + - name: .NET 10 + dotnet_version: '' + experimental: false + # .NET 11 is still a preview SDK band, so a failure here must not block + # merges to the .NET 10 branch. It builds the workload against the .NET 11 + # SDK and runs the net11.0-* matrix rows end to end. + - name: .NET 11 preview + dotnet_version: '11.0.100-preview.7.26381.103' + experimental: true steps: - uses: actions/checkout@v3 with: @@ -71,6 +91,7 @@ jobs: - name: Run make test-matrix env: PULLREQUEST_ID: ${{ github.event.number }} + DOTNET_VERSION: ${{ matrix.dotnet_version }} working-directory: ./workload run: make test-matrix @@ -78,7 +99,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: test-matrix-logs + name: test-matrix-logs-${{ matrix.name }} path: | workload/.tmp/matrix/**/build.log workload/.tmp/matrix/**/dotnet-new.log diff --git a/.github/workflows/build-workload.yml b/.github/workflows/build-workload.yml index 067d51f3e..e76c6d81d 100644 --- a/.github/workflows/build-workload.yml +++ b/.github/workflows/build-workload.yml @@ -5,6 +5,7 @@ on: branches: - main - net10.0 + - net11.0 paths: - 'workload/**' - '.github/workflows/**' @@ -12,6 +13,7 @@ on: branches: - main - net10.0 + - net11.0 paths: - 'workload/**' - '.github/workflows/**' diff --git a/.github/workflows/validate-version-map.yml b/.github/workflows/validate-version-map.yml index 2373ed29d..df47d9e58 100644 --- a/.github/workflows/validate-version-map.yml +++ b/.github/workflows/validate-version-map.yml @@ -10,7 +10,7 @@ name: Validate Version Map on: push: - branches: [ main, net7.0, net8.0, net9.0, net10.0 ] + branches: [ main, net7.0, net8.0, net9.0, net10.0, net11.0 ] paths: - 'workload/scripts/version-map.json' - 'workload/scripts/workload-install.sh' diff --git a/workload/Makefile b/workload/Makefile index c3b5843e7..c55837bb7 100644 --- a/workload/Makefile +++ b/workload/Makefile @@ -141,6 +141,21 @@ test-matrix: install bash $(TOP)/scripts/test-matrix.sh +# Static checks that need no dotnet install: cross-file metadata consistency and +# the SDK feature-band detection shared by both install scripts. +.PHONY: validate-metadata +validate-metadata: + @python3 $(TOP)/scripts/validate-workload-metadata.py + +.PHONY: test-version-band +test-version-band: + @bash $(TOP)/scripts/test-version-band.sh + +.PHONY: check +check: validate-metadata test-version-band + @pwsh $(TOP)/scripts/Generate-InstallScripts.ps1 -Check + + # Remove artifacts and temporary files clean: @rm -fr $(OUTDIR) diff --git a/workload/NuGet.config b/workload/NuGet.config index efc78a639..f4dd8ea2e 100644 --- a/workload/NuGet.config +++ b/workload/NuGet.config @@ -10,6 +10,7 @@ + @@ -20,5 +21,10 @@ --> - + + + + diff --git a/workload/README.md b/workload/README.md index 850eec71d..e969c3bd4 100644 --- a/workload/README.md +++ b/workload/README.md @@ -1,6 +1,17 @@ # Workload for Tizen .NET -This is a build of Tizen workload for an early preview of Tizen in .NET 10. - +This is a build of Tizen workload for Tizen in .NET 10, and for the .NET 11 preview SDK band. + +See [docs/net11.md](docs/net11.md) for the .NET 11 target framework / API-level mapping, +how to build that band, and the external artifacts it is still blocked on. + +## Local checks + +```sh +make check # metadata consistency + version-band tests + install-script drift +make test # single-TFM smoke test (needs a bootstrapped SDK) +make test-matrix # full TFM matrix +``` + ## Using IDEs Refer [here](https://github.com/dotnet/net6-mobile-samples#using-ides) to see the supporting status of an each IDE and how to manually enable workload. diff --git a/workload/build/Versions.props b/workload/build/Versions.props index 79e761b67..3b1b4db8d 100644 --- a/workload/build/Versions.props +++ b/workload/build/Versions.props @@ -54,4 +54,14 @@ 10.0.0-beta.25531.102 + + + + 11.0.0-beta.26426.103 + diff --git a/workload/docs/net11.md b/workload/docs/net11.md new file mode 100644 index 000000000..bb07835bd --- /dev/null +++ b/workload/docs/net11.md @@ -0,0 +1,113 @@ +# .NET 11 support + +This branch builds the `tizen` workload for the .NET 11 SDK band in addition to .NET 10. + +At the time of writing .NET 11 is in **preview**: the newest SDK is +`11.0.100-preview.7.26381.103` (released 2026-08-11). Everything below therefore describes a +band that is real and buildable, but whose manifest package has **not been published to +nuget.org yet**. + +## TFM and API mapping + +The primary target framework is **`net11.0-tizen11.0`**. + +| Tizen platform version | TizenFX API level | Targeting (ref) pack | Pack version | +|---|---|---|---| +| `tizen8.0` | 11 | `Samsung.Tizen.Ref.API11` | `$(TizenFXAPI11Version)` | +| `tizen9.0` | 12 | `Samsung.Tizen.Ref.API12` | `$(TizenFXAPI12Version)` | +| `tizen10.0` | 13 | `Samsung.Tizen.Ref.API13` | `$(TizenFXAPI13Version)` | +| `tizen10.1` | 14 | `Samsung.Tizen.Ref.API14` | `$(TizenFXAPI14Version)` | +| **`tizen11.0`** | **15** | **`Samsung.Tizen.Ref.API15`** | `$(TizenFXAPI15Version)` | + +The .NET version axis and the Tizen platform axis are independent, so `net11.0` combines with +every platform version above — `net11.0-tizen8.0` … `net11.0-tizen11.0` are all valid. The +`tizen-manifest.xml` `api-version` attribute must match the platform version +(`tizen11.0` → `api-version="11"`, `tizen10.1` → `10.1`, `tizen10.0` → `10`). + +**No new reference pack is required for .NET 11.** Ref packs ship reference assemblies under +`ref/net8.0/` and are resolved by explicit `` entries in `data/FrameworkList.xml`, +so they are independent of the consuming project's .NET version. `Samsung.Tizen.Ref.API16` does +not exist and is not needed. + +Use versioned TFMs. The unversioned `net11.0-tizen` form is still accepted as project input but +resolves to `_DefaultTargetPlatformVersion` (`10.0`), which is rarely what a caller wants. + +## Building the .NET 11 band + +`DOTNET_VERSION` selects the SDK band; it defaults to +`MicrosoftDotnetSdkInternalPackageVersion` in `build/Versions.props` (currently the .NET 10 band). + +```sh +# Produce the packs, including Samsung.NET.Sdk.Tizen.Manifest-11.0.100-preview.7 +make packs DOTNET_VERSION=11.0.100-preview.7.26381.103 + +# Install into the locally bootstrapped SDK and run the full TFM matrix +make test-matrix DOTNET_VERSION=11.0.100-preview.7.26381.103 +``` + +The band string is derived generically from `DOTNET_VERSION`, so `11.0.100-rc.1.*` and the +eventual `11.0.100` GA need no further code change. This is asserted by +[`scripts/test-version-band.sh`](../scripts/test-version-band.sh). + +## What changed in this repository + +| File | Change | +|---|---| +| `build/Versions.props` | `MicrosoftDotNetBuildTasksFeedPackageVersion` = `11.0.0-beta.26426.103` when building an `11.0` band | +| `NuGet.config` | added the `dotnet11` package source | +| `src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.targets` | added the `net11.0` `KnownRuntimePack` | +| `src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml` | added the `.NET Runtime 11` `FileList` row | +| `src/Samsung.Tizen.Templates/.../template.json` | added the `net11.0` framework choice (default stays `net10.0`) | +| `scripts/test-matrix.sh` | added `net11.0-tizen11.0` / `net11.0-tizen10.0` rows, skipped when no 11.x SDK is present | +| `scripts/validate-workload-metadata.py` | new checks C5/C6 tying template + matrix TFMs to `KnownRuntimePack` and `RuntimeList.xml` | +| `scripts/test-version-band.sh` | new — asserts SDK-version → feature-band mapping for both installers | +| `.github/workflows/build-matrix.yml` | added a non-blocking .NET 11 preview matrix leg | + +`version-map.json` is deliberately **not** updated. That table is a fallback cache of *already +published* manifest versions, consulted only when the NuGet lookup fails. Adding an entry for a +band that has never been released would make `workload-install.sh` download a 404. The +`11.0.100-preview.7` entry should be added in a follow-up commit *after* the first release, the +same way `10.0.300` was. + +## External blockers + +These artifacts are owned by other repositories. Nothing in Samsung/Tizen.NET can fix them, and +they are not faked here. + +### 1. `Samsung.NET.Sdk.Tizen.Manifest-11.0.100-preview.7` is unpublished + +- **Owner:** Samsung/Tizen.NET maintainers (this repo's `Release Workload` workflow). +- **Action:** run `Release Workload` with `net_sdk_version = 11.0.100-preview.7.26381.103`. +- **Until then:** `workload-install.sh` on an 11.x SDK finds no manifest. Local development works + via `make install DOTNET_VERSION=11.0.100-preview.7.26381.103`, which installs into the + bootstrapped SDK under `workload/out/dotnet`. + +### 2. `Tizen.UIExtensions.NUI` has no modern assets + +- **Owner:** [Samsung/Tizen.UIExtensions](https://github.com/Samsung/Tizen.UIExtensions). +- **Published state:** `0.9.2` ships `lib/net6.0-tizen7.0/` and `lib/tizen10.0/`. Source on `main` + targets `tizen10.0;net6.0-tizen` against + `Tizen.NET 10.0.0.17508` (API level 10). +- **Note:** NuGet TFM compatibility is *not* the problem — `net6.0-tizen7.0` is consumable from + `net11.0-tizen11.0`. The blocker is the dependency group, which pins + `Microsoft.Maui.Graphics` / `Microsoft.Maui.Graphics.Skia` `6.0.300-rc.3.1336` and + `SkiaSharp.Views 2.88.6`, dragging .NET 6-era MAUI Graphics into any modern MAUI build. +- **Expected artifact:** a `Tizen.UIExtensions.NUI` release with a `lib/net11.0-tizen11.0/` folder + built against `Samsung.Tizen.Ref.API15` and a refreshed `Microsoft.Maui.Graphics*` dependency. +- **API risk, measured:** all types .NET MAUI's Tizen backend needs are present in API 15. Between + API 11 and API 15, `Tizen.NUI.ScrollView`, `Tizen.NUI.ItemView` (and the `Item*`/`Ruler*` + families), `Tizen.NUI.Components.Title`, `Tizen.NUI.Adaptor`, `Tizen.NUI.AutofillContainer`, + `Tizen.NUI.Accessibility.AccessibilityManager`, the `CubeTransition*` effects and the entire + `Tizen.NUI.Wearable` namespace were removed. Ports should use + `Tizen.NUI.Components.ScrollableBase`, which is retained. + +### 3. `Tizen.UI.Components.Material` is platform-gated + +- **Owner:** TizenAPI / Samsung (package `Tizen.UI.Components.Material`). +- **Published state:** `1.0.0-rc.8` ships **only** `lib/net8.0-tizen10.0/`; never went GA. +- **Effect:** consumable from `-tizen10.0`, `-tizen10.1` and `-tizen11.0`, but not from + `-tizen8.0` / `-tizen9.0`. The `dotnet new tizen` template now references it conditionally and + raises `TIZENTMPL001` with an actionable message instead of an opaque restore failure when the + target platform is below `tizen10.0`. +- **Expected artifact:** a release with `lib/` assets for the lower platform bands, or a separate + non-Material template family for them. diff --git a/workload/scripts/README.md b/workload/scripts/README.md index 207f4c852..8b9dd14d6 100644 --- a/workload/scripts/README.md +++ b/workload/scripts/README.md @@ -51,6 +51,34 @@ To add or update an entry: The CI workflow `validate-version-map.yml` runs `Generate-InstallScripts.ps1 -Check` on every PR and fails if the two scripts have drifted from `version-map.json`. +### When *not* to add an entry + +`LatestVersionMap` is a **fallback cache of already-published manifest versions**. It is only +consulted when the live NuGet lookup fails. Adding an entry for an SDK band whose +`Samsung.NET.Sdk.Tizen.Manifest-` package has never been released makes the installer +download a 404. Add the entry *after* the release, not before — see commit +`chore: add 10.0.300 -> 10.0.127 to version map`. + +## test-version-band + +`Generate-InstallScripts.ps1 -Check` only compares the generated version-map block, so it cannot +see problems elsewhere in the installers. Two extra guards cover that gap: + +* [`test-version-band.sh`](./test-version-band.sh) asserts the SDK-version → feature-band mapping + (for example `11.0.100-preview.7.26381.103` → `11.0.100-preview.7`) and checks that + `workload-install.sh` and `workload-install.ps1` agree. The bash implementation is extracted + live from `workload-install.sh` between the `BEGIN/END VERSION BAND DETECTION` markers, so the + test always exercises shipped code — keep those markers intact. +* `Generate-InstallScripts.ps1` additionally verifies both installers contain no NUL bytes and + end with their expected final statement. Both scripts had previously been committed truncated + mid-statement and NUL-padded, which the version-map drift check reported as "OK". + +Run everything at once with: + +``` +make -C workload check +``` + ### Why Previously, the same ~36 entries were maintained by hand in two different languages diff --git a/workload/scripts/test-matrix.sh b/workload/scripts/test-matrix.sh index e27836766..ef5100169 100644 --- a/workload/scripts/test-matrix.sh +++ b/workload/scripts/test-matrix.sh @@ -37,11 +37,19 @@ ONLY="${TEST_MATRIX_ONLY:-}" # A single shared fixture can't build both eras, so they are excluded here. # Add coverage in a follow-up by introducing a separate legacy fixture # (e.g. workload/scripts/fixtures/legacy/) keyed off the row's TFM. +# +# NOTE on net11.0-*: +# .NET 11 is a preview SDK band. A row is SKIPPED (not failed) when the dotnet +# under test cannot build that .NET major, so the default `make test-matrix` +# run against the .NET 10 band stays green. To exercise these rows: +# make test-matrix DOTNET_VERSION=11.0.100-preview.7.26381.103 MATRIX=( "net8.0-tizen10.0|10" "net8.0-tizen10.1|10.1" "net8.0-tizen11.0|11" "net9.0-tizen10.0|10" + "net11.0-tizen11.0|11" + "net11.0-tizen10.0|10" ) # --- helpers --------------------------------------------------------------- @@ -53,6 +61,24 @@ log() { printf "%s\n" "$*"; } pass() { printf " %sPASS%s %s\n" "$c_green" "$c_reset" "$*"; } fail() { printf " %sFAIL%s %s\n" "$c_red" "$c_reset" "$*"; } warn() { printf " %sWARN%s %s\n" "$c_yellow" "$c_reset" "$*"; } +skip() { printf " %sSKIP%s %s\n" "$c_yellow" "$c_reset" "$*"; } + +# Major versions of the .NET SDKs visible to $DOTNET, e.g. "8 9 10". +# Populated once, after the $DOTNET prerequisite check below. +INSTALLED_SDK_MAJORS="" + +# sdk_supports e.g. sdk_supports net11.0 +# An SDK can only build a TFM whose .NET major it ships, so a net11.0 row needs +# an 11.x SDK. Returns non-zero when unavailable so the row can be skipped. +sdk_supports() { + local netver="$1" + local want="${netver#net}"; want="${want%%.*}" + local have + for have in $INSTALLED_SDK_MAJORS; do + [[ "$have" == "$want" ]] && return 0 + done + return 1 +} # --- prerequisites --------------------------------------------------------- @@ -71,10 +97,15 @@ fi mkdir -p "$TMPDIR" +# Discover which .NET majors this dotnet can build for. +INSTALLED_SDK_MAJORS="$("$DOTNET" --list-sdks 2>/dev/null | sed -E 's/^([0-9]+)\..*/\1/' | sort -un | tr '\n' ' ')" +log "Installed .NET SDK majors: ${INSTALLED_SDK_MAJORS:-}" + # --- matrix loop ----------------------------------------------------------- -declare -i pass_count=0 fail_count=0 +declare -i pass_count=0 fail_count=0 skip_count=0 declare -a failed_rows=() +declare -a skipped_rows=() for entry in "${MATRIX[@]}"; do tfm="${entry%%|*}" @@ -84,13 +115,20 @@ for entry in "${MATRIX[@]}"; do continue fi - netver="${tfm%-tizen*}" # net6.0 / net8.0 / net9.0 - platver="${tfm##*-tizen}" # 8.0 / 9.0 / 10.0 / 11.0 + netver="${tfm%-tizen*}" # net6.0 / net8.0 / net9.0 / net11.0 + platver="${tfm##*-tizen}" # 8.0 / 9.0 / 10.0 / 10.1 / 11.0 rowdir="$TMPDIR/$tfm" log "" log "==> [$tfm] api-version=$apiver" + if ! sdk_supports "$netver"; then + skip "$tfm (no ${netver} SDK installed; rerun with DOTNET_VERSION=<${netver#net} sdk>)" + skip_count+=1 + skipped_rows+=("$tfm") + continue + fi + rm -rf "$rowdir" mkdir -p "$rowdir" @@ -157,6 +195,14 @@ log "" log "================ test-matrix summary ================" log " passed: $pass_count" log " failed: $fail_count" +log " skipped: $skip_count" + +if [[ $skip_count -gt 0 ]]; then + log " skipped rows:" + for r in "${skipped_rows[@]}"; do + log " - $r" + done +fi if [[ $fail_count -gt 0 ]]; then log " failed rows:" diff --git a/workload/scripts/test-version-band.sh b/workload/scripts/test-version-band.sh new file mode 100755 index 000000000..adc1e2939 --- /dev/null +++ b/workload/scripts/test-version-band.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# +# Copyright (c) Samsung Electronics. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# +# Unit test for the SDK feature-band detection used by the install scripts. +# +# The band string decides three things at install time: +# * the NuGet package id Samsung.NET.Sdk.Tizen.Manifest- +# * the manifest directory /sdk-manifests//samsung.net.sdk.tizen +# * the version-map key $MANIFEST_BASE_NAME- +# Getting it wrong silently installs the workload where the SDK will never look, +# so it is worth a real test — especially for new majors such as .NET 11 preview. +# +# The bash implementation is extracted from workload-install.sh between the +# "BEGIN/END VERSION BAND DETECTION" markers so the test always exercises the +# shipped code rather than a copy. +# +# Usage: +# bash workload/scripts/test-version-band.sh +# make -C workload test-version-band +# + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SH_SCRIPT="$SCRIPT_DIR/workload-install.sh" +PS1_SCRIPT="$SCRIPT_DIR/workload-install.ps1" + +c_reset=$'\033[0m'; c_red=$'\033[31m'; c_green=$'\033[32m' +[[ -t 1 ]] || { c_reset=""; c_red=""; c_green=""; } + +# --- load the function under test ------------------------------------------ + +if [[ ! -f "$SH_SCRIPT" ]]; then + echo "ERROR: $SH_SCRIPT not found." + exit 2 +fi + +BAND_FUNC="$(sed -n '/# BEGIN VERSION BAND DETECTION/,/# END VERSION BAND DETECTION/p' "$SH_SCRIPT")" +if [[ -z "$BAND_FUNC" ]]; then + echo "ERROR: VERSION BAND DETECTION markers not found in $SH_SCRIPT." + echo " The install script was edited without keeping the markers intact." + exit 2 +fi +eval "$BAND_FUNC" + +# --- cases: "|" -------------------------------- +CASES=( + # .NET 6 has no preview bands: always rounded to the feature band. + "6.0.419|6.0.400" + "6.0.100|6.0.100" + # Stable bands. + "7.0.400|7.0.400" + "8.0.404|8.0.400" + "9.0.304|9.0.300" + "10.0.100|10.0.100" + "10.0.400|10.0.400" + # Pre-release bands. + "7.0.100-preview.6.22352.1|7.0.100-preview.6" + "8.0.100-rc.2.23502.2|8.0.100-rc.2" + "8.0.100-rtm.23512.16|8.0.100-rtm" + "9.0.100-alpha.1.23615.4|9.0.100-alpha.1" + "10.0.100-rc.2.25502.107|10.0.100-rc.2" + # .NET 11 — the band this repo now supports. + "11.0.100-preview.7.26381.103|11.0.100-preview.7" + "11.0.100-preview.6.26359.118|11.0.100-preview.6" + "11.0.100-rc.1.26500.1|11.0.100-rc.1" + "11.0.100|11.0.100" + "11.0.200|11.0.200" + # Future majors must keep working without another code change. + "12.0.100-preview.1.27000.1|12.0.100-preview.1" +) + +pass=0; fail=0 + +for case in "${CASES[@]}"; do + version="${case%%|*}" + expected="${case##*|}" + actual="$(compute_target_version_band "$version")" + if [[ "$actual" == "$expected" ]]; then + printf " %sPASS%s %-32s -> %s\n" "$c_green" "$c_reset" "$version" "$actual" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-32s -> %s (expected %s)\n" "$c_red" "$c_reset" "$version" "$actual" "$expected" + fail=$((fail + 1)) + fi +done + +# --- parity check: the PowerShell installer must agree --------------------- +# +# workload-install.ps1 reimplements the same logic. Run the same cases through +# pwsh when it is available so the two installers cannot drift apart. + +if command -v pwsh >/dev/null 2>&1 && [[ -f "$PS1_SCRIPT" ]]; then + echo "" + echo "-- PowerShell parity --" + for case in "${CASES[@]}"; do + version="${case%%|*}" + expected="${case##*|}" + actual="$(pwsh -NoProfile -Command " + \$DotnetVersion = '$version' + \$VersionSplitSymbol = '.' + \$SplitVersion = \$DotnetVersion.Split(\$VersionSplitSymbol) + \$CurrentDotnetVersion = [Version]\"\$(\$SplitVersion[0]).\$(\$SplitVersion[1])\" + \$DotnetVersionBand = \$SplitVersion[0] + \$VersionSplitSymbol + \$SplitVersion[1] + \$VersionSplitSymbol + \$SplitVersion[2][0] + '00' + \$DotnetTargetVersionBand = \$DotnetVersionBand + if (\$CurrentDotnetVersion -ge [Version]'7.0') { + \$IsPreviewVersion = \$DotnetVersion.Contains('-preview') -or \$DotnetVersion.Contains('-rc') -or \$DotnetVersion.Contains('-alpha') + if (\$IsPreviewVersion -and (\$SplitVersion.Count -ge 4)) { + \$DotnetTargetVersionBand = \$DotnetVersionBand + \$SplitVersion[2].SubString(3) + \$VersionSplitSymbol + \$(\$SplitVersion[3]) + } + elseif (\$DotnetVersion.Contains('-rtm') -and (\$SplitVersion.Count -ge 3)) { + \$DotnetTargetVersionBand = \$DotnetVersionBand + \$SplitVersion[2].SubString(3) + } + } + Write-Output \$DotnetTargetVersionBand" 2>/dev/null | tr -d '\r')" + if [[ "$actual" == "$expected" ]]; then + printf " %sPASS%s %-32s -> %s\n" "$c_green" "$c_reset" "$version" "$actual" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-32s -> %s (expected %s)\n" "$c_red" "$c_reset" "$version" "$actual" "$expected" + fail=$((fail + 1)) + fi + done +else + echo "" + echo " (pwsh not available - skipping workload-install.ps1 parity check)" +fi + +echo "" +echo "================ version-band summary ================" +echo " passed: $pass" +echo " failed: $fail" + +[[ $fail -eq 0 ]] || exit 1 +exit 0 diff --git a/workload/scripts/validate-workload-metadata.py b/workload/scripts/validate-workload-metadata.py index ba61c9e3d..23c76e298 100644 --- a/workload/scripts/validate-workload-metadata.py +++ b/workload/scripts/validate-workload-metadata.py @@ -14,6 +14,9 @@ workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.Versions.targets.in workload/src/Samsung.NET.Sdk.Tizen/WorkloadManifest.in.json workload/scripts/test-matrix.sh + workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.targets + workload/src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml + workload/src/Samsung.Tizen.Templates/tizen/.template.config/template.json Checks: C1 Versions.targets.in sigils all have matching Versions.props property @@ -23,6 +26,11 @@ C3 TizenSdkSupportedTargetPlatformVersion ↔ KnownFrameworkReference consistency in Versions.targets.in. C4 test-matrix.sh MATRIX uses only supported platforms. + C5 Every .NET major offered by the template / exercised by test-matrix.sh has a + matching KnownRuntimePack in Samsung.Tizen.Sdk.targets. Without it the SDK + cannot resolve Samsung.NETCore.App.Runtime.tizen and the build fails with + NETSDK1082 ("no runtime pack available"). + C6 Every KnownRuntimePack .NET major has a FileList row in RuntimeList.xml. Run from anywhere — paths derive from this script's location. @@ -85,6 +93,9 @@ def warn(msg): versions_in = read("src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.Versions.targets.in") workload_in = read("src/Samsung.NET.Sdk.Tizen/WorkloadManifest.in.json") matrix_sh = read("scripts/test-matrix.sh") + sdk_targets = read("src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.targets") + runtime_list = read("src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml") + template_json = read("src/Samsung.Tizen.Templates/tizen/.template.config/template.json") sdk_repl = parse_replacements(sdk_proj) manifest_repl = parse_replacements(manifest_proj) @@ -162,6 +173,49 @@ def warn(msg): else: ok("C4: test-matrix.sh (" + str(len(matrix_entries)) + " rows) all platforms supported") + # --- C5 --- + # KnownRuntimePack is keyed by the .NET major TFM (net8.0, net10.0, ...). + # A TFM the template can produce but the SDK has no runtime pack for fails at + # build time, so both producers of TFMs must be covered. + krp_tfms = set( + re.findall(r']*?\n?[^>]*?TargetFramework="(net[\d.]+)"', sdk_targets) + ) + if not krp_tfms: + # Attribute order/newlines vary; fall back to a per-element scan. + for block in re.findall(r"", sdk_targets, re.S): + m = re.search(r'TargetFramework="(net[\d.]+)"', block) + if m: + krp_tfms.add(m.group(1)) + + template_netvers = set(re.findall(r'"choice":\s*"(net[\d.]+)"', template_json)) + matrix_netvers = {tfm.split("-tizen", 1)[0] for tfm, _api in matrix_entries} + required_netvers = template_netvers | matrix_netvers + + missing_krp = sorted(required_netvers - krp_tfms, key=lambda v: float(v[3:])) + if not krp_tfms: + err("C5: no KnownRuntimePack entries parsed from Samsung.Tizen.Sdk.targets") + elif missing_krp: + err("C5: no KnownRuntimePack for " + str(missing_krp) + + " (offered by template.json / test-matrix.sh). Add a KnownRuntimePack " + "entry in Samsung.Tizen.Sdk.targets or the build fails with NETSDK1082.") + else: + ok("C5: template/test-matrix .NET majors (" + str(len(required_netvers)) + + ") all have a KnownRuntimePack") + + # --- C6 --- + runtime_list_vers = { + "net" + v for v in re.findall(r'TargetFrameworkVersion="([\d.]+)"', runtime_list) + } + missing_rl = sorted(krp_tfms - runtime_list_vers, key=lambda v: float(v[3:])) if krp_tfms else [] + if not runtime_list_vers: + err("C6: no FileList rows parsed from Samsung.NETCore.App.Runtime/data/RuntimeList.xml") + elif missing_rl: + err("C6: RuntimeList.xml has no FileList row for " + str(missing_rl) + + " but a KnownRuntimePack declares it.") + else: + ok("C6: KnownRuntimePack majors (" + str(len(krp_tfms)) + + ") all present in RuntimeList.xml") + print() if errors: print("==== " + str(len(errors)) + " ERROR(S) ====") diff --git a/workload/src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml b/workload/src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml index a95fbb540..c188d2592 100644 --- a/workload/src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml +++ b/workload/src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml @@ -3,3 +3,4 @@ + diff --git a/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.targets b/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.targets index 1d09a5715..89d487ec9 100644 --- a/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.targets +++ b/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.targets @@ -71,6 +71,14 @@ Copyright (c) Samsung All rights reserved. RuntimePackRuntimeIdentifiers="tizen" RuntimePackLabels="Tizen" /> + diff --git a/workload/src/Samsung.Tizen.Templates/tizen/.template.config/template.json b/workload/src/Samsung.Tizen.Templates/tizen/.template.config/template.json index 5893e7630..e25bbda55 100644 --- a/workload/src/Samsung.Tizen.Templates/tizen/.template.config/template.json +++ b/workload/src/Samsung.Tizen.Templates/tizen/.template.config/template.json @@ -46,6 +46,10 @@ { "choice": "net10.0", "description": "target framework version is net10.0-tizen" + }, + { + "choice": "net11.0", + "description": "target framework version is net11.0-tizen (requires a .NET 11 SDK)" } ], "replaces": "net10.0", diff --git a/workload/src/Samsung.Tizen.Templates/tizen/TizenApp1.csproj b/workload/src/Samsung.Tizen.Templates/tizen/TizenApp1.csproj index 1cfeef6cb..85082a1aa 100644 --- a/workload/src/Samsung.Tizen.Templates/tizen/TizenApp1.csproj +++ b/workload/src/Samsung.Tizen.Templates/tizen/TizenApp1.csproj @@ -5,7 +5,26 @@ Exe - + + + <_TizenUIPlatformVersion>$(TargetPlatformVersion) + <_TizenUIPlatformVersion Condition="'$(_TizenUIPlatformVersion)' == ''">10.0 + <_TizenUIMaterialSupported>$([MSBuild]::VersionGreaterThanOrEquals('$(_TizenUIPlatformVersion)', '10.0')) + + + + + + + From 9cdce6843ef90ebe7a9c386d31505c534860de4f Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 26 Aug 2026 15:50:34 -0400 Subject: [PATCH 3/7] fix: address code review and MSBuild review findings Code review: * test-matrix.sh required an exact SDK/TFM major match, so the .NET 10 job skipped every net8/net9/net11 row and built nothing. An SDK builds its own major and all earlier ones, so rows are now skipped only when NEWER than the newest installed SDK. Pinned by a new `--self-test` mode that needs no dotnet install. * build-matrix.yml made .NET 11 unconditionally advisory and build-workload.yml always used the Versions.props default. Both now switch on a net11.0 branch or a PR into one: the .NET 11 leg blocks and the workload builds against $(DotNet11SdkVersion), a new SSOT in Versions.props that the workflows grep. Check C7 fails if a workflow hardcodes a different .NET 11 SDK. * The template read $(TargetPlatformVersion) in the project body, which the SDK has not yet inferred from the TFM at that point. net11.0-tizen9.0 therefore fell back to 10.0, pulled in an incompatible Tizen.UI.Components.Material and never raised TIZENTMPL001. The platform version is now parsed from $(TargetFramework), and the target re-checks the authoritative value. Verified end to end: tizen9.0 now errors with the real resolved version, tizen11.0 builds. * Both installers swallowed per-SDK failures and exited 0 after printing DONE. They now track failures and exit non-zero; --update-all-workloads still visits the remaining SDKs but the overall run fails. MSBuild review: * workload-install.ps1 built its fallback prefix from a fixed length ($ManifestBaseName.Length + 2), so '...Manifest-11.0.100-preview.7' became '...Manifest-1' and matched the 10.x entries, installing a .NET 10 manifest into an 11.x band. Both installers now constrain the fallback to the same major.minor family and fail closed. * DOTNET_VERSION was cached in .tmp/dotnet-version.config whose only prerequisite was Versions.props. The cache was always newer, so make never regenerated it and a newly-passed DOTNET_VERSION was ignored - silently building the previous band. It is now resolved immediately, and DOTNET_DESTDIR plus the install stamp are band-scoped. * PackageTargetFallback now covers the full (.NET major x platform) cross-product. A missing entry makes FixupNuGetReferences leave a package on its netstandard2.x assets; net11.0-tizen11.0 was absent. Check C8 keeps it in sync with KnownRuntimePack x TizenSdkSupportedTargetPlatformVersion. * release-workload.yml no longer builds with continue-on-error and no longer pushes a glob over whatever is on disk. It cleans first, fails on build error, stages to an isolated directory and verifies the expected manifest exists before pushing. * test-version-band.sh now extracts the real Get-TargetVersionBand from workload-install.ps1 instead of reimplementing it, so sh/ps1 drift is actually detectable, and also checks Config.mk producer parity. * Config.mk did not round the feature band for stable non-6 versions: 10.0.404 produced band '10.0.404' while the installers looked for '10.0.400'. Fixed, with producer/consumer parity tests. * MSI staging copied only Ref.API11/12/13; API14 and API15 were missing and API15 is the targeting pack for net11.0-tizen11.0. Now stages every Ref pack. * workload-install.sh had its shebang below a comment block, so direct execution did not reliably use bash. Moved to the first line. * make check now gates on pwsh with an actionable message instead of failing with 'command not found'; SKIP_PWSH_CHECKS=1 opts out. RuntimeList.xml (multiple root elements) is left as-is: proven not to be XML-parsed. Replacing the installed pack's copy with non-XML text still builds a net11.0-tizen11.0 project with an explicit RuntimeIdentifier - the path that resolves runtime pack assets and the one MAUI takes. Documented in workload/docs/net11.md. Validation on 11.0.100-preview.7.26381.103: full matrix 6/6 pass, 0 skipped (net8.0-tizen10.0/10.1/11.0, net9.0-tizen10.0, net11.0-tizen11.0, net11.0-tizen10.0). make check: C1-C8, 11 self-test, 61 version-band, 13 template-condition, 4 install-failure assertions, plus install-script drift and integrity. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-matrix.yml | 32 +++- .github/workflows/build-workload.yml | 21 +++ .github/workflows/release-workload.yml | 48 +++++- workload/Config.mk | 42 ++++-- workload/Makefile | 49 +++++- workload/build/Versions.props | 9 +- workload/docs/net11.md | 122 ++++++++++++++- workload/scripts/README.md | 22 ++- workload/scripts/test-install-failure.sh | 96 ++++++++++++ workload/scripts/test-matrix.sh | 76 ++++++++-- workload/scripts/test-template-conditions.sh | 117 +++++++++++++++ workload/scripts/test-version-band.sh | 141 +++++++++++++++--- .../scripts/validate-workload-metadata.py | 46 ++++++ workload/scripts/workload-install.ps1 | 130 ++++++++++------ workload/scripts/workload-install.sh | 49 ++++-- .../targets/Samsung.Tizen.Sdk.NuGet.targets | 35 ++++- .../tizen/TizenApp1.csproj | 31 +++- 17 files changed, 914 insertions(+), 152 deletions(-) create mode 100755 workload/scripts/test-install-failure.sh create mode 100755 workload/scripts/test-template-conditions.sh diff --git a/.github/workflows/build-matrix.yml b/.github/workflows/build-matrix.yml index 14cd73a30..41b9b3e4e 100644 --- a/.github/workflows/build-matrix.yml +++ b/.github/workflows/build-matrix.yml @@ -39,9 +39,22 @@ jobs: validate-metadata: name: Validate workload metadata runs-on: ubuntu-22.04 + outputs: + net11_sdk: ${{ steps.versions.outputs.net11_sdk }} steps: - uses: actions/checkout@v3 + - name: Resolve .NET 11 SDK version from Versions.props + id: versions + run: | + NET11=$(grep -oP '(?<=)[^<]+' workload/build/Versions.props) + if [ -z "$NET11" ]; then + echo "::error::DotNet11SdkVersion not found in workload/build/Versions.props" + exit 1 + fi + echo "net11_sdk=$NET11" >> "$GITHUB_OUTPUT" + echo "::notice ::.NET 11 SDK for CI: $NET11" + - name: Authenticate GitHub Packages NuGet source run: | dotnet nuget update source github \ @@ -53,27 +66,34 @@ jobs: - name: Run validate-workload-metadata.py run: python3 workload/scripts/validate-workload-metadata.py + - name: Run test-matrix.sh --self-test + run: bash workload/scripts/test-matrix.sh --self-test + - name: Run test-version-band.sh run: bash workload/scripts/test-version-band.sh + - name: Run test-template-conditions.sh + run: bash workload/scripts/test-template-conditions.sh + + - name: Run test-install-failure.sh + run: bash workload/scripts/test-install-failure.sh + test-matrix: name: Multi-TFM build matrix (${{ matrix.name }}) needs: validate-metadata runs-on: ubuntu-22.04 - continue-on-error: ${{ matrix.experimental }} + # The .NET 11 leg is advisory only while the branch targets a shipping band. On a + # net11.0 branch (or a PR into one) .NET 11 is the product, so it must block. + continue-on-error: ${{ matrix.experimental && !(github.ref_name == 'net11.0' || github.base_ref == 'net11.0') }} strategy: fail-fast: false matrix: include: - # Shipping band. Blocking. - name: .NET 10 dotnet_version: '' experimental: false - # .NET 11 is still a preview SDK band, so a failure here must not block - # merges to the .NET 10 branch. It builds the workload against the .NET 11 - # SDK and runs the net11.0-* matrix rows end to end. - name: .NET 11 preview - dotnet_version: '11.0.100-preview.7.26381.103' + dotnet_version: ${{ needs.validate-metadata.outputs.net11_sdk }} experimental: true steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/build-workload.yml b/.github/workflows/build-workload.yml index e76c6d81d..165ea29b5 100644 --- a/.github/workflows/build-workload.yml +++ b/.github/workflows/build-workload.yml @@ -43,10 +43,31 @@ jobs: --store-password-in-clear-text \ --configfile workload/NuGet.config + # On a net11.0 branch (or a PR into one) .NET 11 is the product being built, so the + # workload must be built against the .NET 11 SDK rather than the Versions.props + # default. DotNet11SdkVersion in Versions.props is the single source of truth. + - name: Select target SDK band + id: sdk + run: | + TARGET_BRANCH="${{ github.base_ref || github.ref_name }}" + if [ "$TARGET_BRANCH" = "net11.0" ]; then + NET11=$(grep -oP '(?<=)[^<]+' workload/build/Versions.props) + if [ -z "$NET11" ]; then + echo "::error::DotNet11SdkVersion not found in workload/build/Versions.props" + exit 1 + fi + echo "dotnet_version=$NET11" >> "$GITHUB_OUTPUT" + echo "::notice ::Branch '$TARGET_BRANCH' builds against .NET 11 SDK $NET11" + else + echo "dotnet_version=" >> "$GITHUB_OUTPUT" + echo "::notice ::Branch '$TARGET_BRANCH' builds against the Versions.props default SDK band" + fi + - name: Build env: PULLREQUEST_ID: ${{ github.event.number }} PRERELEASE_TAG: ${{ github.event.inputs.prerelease }} + DOTNET_VERSION: ${{ steps.sdk.outputs.dotnet_version }} run: make test working-directory: ./workload diff --git a/.github/workflows/release-workload.yml b/.github/workflows/release-workload.yml index 0ddb29d79..a24cf35f3 100644 --- a/.github/workflows/release-workload.yml +++ b/.github/workflows/release-workload.yml @@ -57,33 +57,69 @@ jobs: - name: Install Wix toolset run: sudo apt-get install -y wixl + # A release must never publish a half-built or stale set of packages. The tree is + # cleaned first so the output directory can only contain this run's artifacts, and + # the build is NOT continue-on-error: a failed build must abort the release. + - name: Clean previous output + run: make clean + working-directory: ./workload + - name: Build env: PRERELEASE_TAG: "stable" run: make install -d DOTNET_VERSION=${{ github.event.inputs.net_sdk_version }} working-directory: ./workload - continue-on-error: true + + # Stage into an isolated directory and verify the expected artifacts exist, so the + # push steps operate on an explicit, audited list rather than a glob over whatever + # happens to be on disk. + - name: Stage and verify packages + id: stage + run: | + set -euo pipefail + STAGING="$RUNNER_TEMP/staging" + rm -rf "$STAGING" && mkdir -p "$STAGING" + shopt -s nullglob + pkgs=(./workload/out/nuget-unsigned/*.nupkg) + if [ ${#pkgs[@]} -eq 0 ]; then + echo "::error::Build produced no packages." + exit 1 + fi + cp "${pkgs[@]}" "$STAGING/" + SDK="${{ github.event.inputs.net_sdk_version }}" + # Reuse the band function from workload-install.sh so the release cannot + # disagree with what the installer will look for. + eval "$(sed -n '/# BEGIN VERSION BAND DETECTION/,/# END VERSION BAND DETECTION/p' workload/scripts/workload-install.sh)" + BAND="$(compute_target_version_band "$SDK")" + echo "Resolved SDK feature band: $BAND" + if ! ls "$STAGING/Samsung.NET.Sdk.Tizen.Manifest-$BAND."*.nupkg >/dev/null 2>&1; then + echo "::error::No Samsung.NET.Sdk.Tizen.Manifest-$BAND package was produced." + ls -1 "$STAGING" + exit 1 + fi + echo "staging=$STAGING" >> "$GITHUB_OUTPUT" + echo "Staged packages:"; ls -1 "$STAGING" - name: Push Manifest/SDK/Runtime packs if: ${{ github.event.inputs.release_manifest == 'true' }} run: | echo "Pushing Manifest packs for version ${{ github.event.inputs.net_sdk_version }}"... - dotnet nuget push ./workload/out/nuget-unsigned/Samsung.NET.Sdk.Tizen.Manifest-*.nupkg \ + dotnet nuget push ${{ steps.stage.outputs.staging }}/Samsung.NET.Sdk.Tizen.Manifest-*.nupkg \ -k ${{ secrets.NUGET_APIKEY }} \ -s https://api.nuget.org/v3/index.json \ -t 3000 \ --skip-duplicate - dotnet nuget push ./workload/out/nuget-unsigned/Samsung.Tizen.Sdk.*.nupkg \ + dotnet nuget push ${{ steps.stage.outputs.staging }}/Samsung.Tizen.Sdk.*.nupkg \ -k ${{ secrets.NUGET_APIKEY }} \ -s https://api.nuget.org/v3/index.json \ -t 3000 \ --skip-duplicate - dotnet nuget push ./workload/out/nuget-unsigned/Samsung.NETCore.App.Runtime.*.nupkg \ + dotnet nuget push ${{ steps.stage.outputs.staging }}/Samsung.NETCore.App.Runtime.*.nupkg \ -k ${{ secrets.NUGET_APIKEY }} \ -s https://api.nuget.org/v3/index.json \ -t 3000 \ --skip-duplicate - dotnet nuget push ./workload/out/nuget-unsigned/Samsung.Tizen.Templates.*.nupkg \ + dotnet nuget push ${{ steps.stage.outputs.staging }}/Samsung.Tizen.Templates.*.nupkg \ -k ${{ secrets.NUGET_APIKEY }} \ -s https://api.nuget.org/v3/index.json \ -t 3000 \ @@ -93,7 +129,7 @@ jobs: if: ${{ github.event.inputs.release_reference == 'true' }} run: | echo "Pushing Manifest packs for version ${{ github.event.inputs.net_sdk_version }}"... - dotnet nuget push ./workload/out/nuget-unsigned/Samsung.Tizen.Ref.*.nupkg \ + dotnet nuget push ${{ steps.stage.outputs.staging }}/Samsung.Tizen.Ref.*.nupkg \ -k ${{ secrets.NUGET_APIKEY }} \ -s https://api.nuget.org/v3/index.json \ -t 3000 \ diff --git a/workload/Config.mk b/workload/Config.mk index bf0ba66d4..d2592266b 100644 --- a/workload/Config.mk +++ b/workload/Config.mk @@ -1,12 +1,13 @@ # DOTNET_VERSION --include $(TMPDIR)/dotnet-version.config -$(TMPDIR)/dotnet-version.config: $(TOP)/build/Versions.props -ifeq ($(DOTNET_VERSION), ) - @mkdir -p $(TMPDIR) - @grep "" build/Versions.props | sed -e 's/<\/*MicrosoftDotnetSdkInternalPackageVersion>//g' -e 's/[ \t]*/DOTNET_VERSION=/' > $@ -else - @mkdir -p $(TMPDIR) - @echo "DOTNET_VERSION=$(DOTNET_VERSION)" > $@ +# +# Resolved immediately from the caller or, when unset, from Versions.props. +# +# This used to be cached in $(TMPDIR)/dotnet-version.config with Versions.props as its only +# prerequisite. Because the cache file was then newer than Versions.props, make never +# regenerated it, so a DIFFERENT DOTNET_VERSION passed into an existing tree was ignored and +# the previous band's value was reused - silently building/testing the wrong band. +ifeq ($(strip $(DOTNET_VERSION)),) +DOTNET_VERSION := $(shell grep -oE '[^<]+' $(TOP)/build/Versions.props | sed 's/.*>//') endif # TizenFX API versions per API level — auto-extracted from Versions.props (SSOT) @@ -18,6 +19,13 @@ $(TMPDIR)/tizen-fx-api-versions.config: $(TOP)/build/Versions.props $(info DOTNET_VERSION is.. $(DOTNET_VERSION)) +# NOTE: do not add a parse-time guard for an empty DOTNET_VERSION here. The value arrives +# via `-include $(TMPDIR)/dotnet-version.config`, and on make's first parse pass (before it +# regenerates that file and restarts) DOTNET_VERSION is legitimately empty. +# Pass DOTNET_VERSION through the environment or omit it entirely; an explicit empty +# command-line override (make DOTNET_VERSION=) wins over the generated file and yields an +# empty band. + DOTNET_VERSION_BAND = $(firstword $(subst -, ,$(DOTNET_VERSION))) IS_PRERELEASE=$(findstring -,$(DOTNET_VERSION)) @@ -30,31 +38,35 @@ endif MAJOR = $(word 1,$(VERSIONS)) MINOR = $(word 2,$(VERSIONS)) MICRO = $(word 3,$(VERSIONS)) -BAND := $(shell echo "${MICRO}" | cut -c1)00 +# Feature band: the patch component rounded down to the nearest hundred (404 -> 400). +BAND = $(shell echo "$(MICRO)" | cut -c1)00 PRERELEASE = $(word 4,$(VERSIONS)) PRERELEASE_VERSION = $(word 5,$(VERSIONS)) # DOTNET_DESTDIR ifeq ($(DESTDIR),) - DOTNET_DESTDIR = $(OUTDIR)/dotnet + # Band-scoped: `make install DOTNET_VERSION=11...` in a tree that already built the + # .NET 10 band must bootstrap a separate SDK instead of reusing (and silently + # testing) the old one. + DOTNET_DESTDIR = $(OUTDIR)/dotnet-$(DOTNET_VERSION_BAND) else DOTNET_DESTDIR = $(abspath $(DESTDIR)) endif ifeq ($(MAJOR),6) - DOTNET6_MANIFESTS_DESTDIR := $(MAJOR).$(MINOR).$(BAND) - DOTNET_MANIFESTS_DESTDIR := $(DOTNET_DESTDIR)/sdk-manifests/$(DOTNET6_MANIFESTS_DESTDIR)/samsung.net.sdk.tizen DOTNET_VERSION_BAND := $(MAJOR).$(MINOR).$(BAND) + DOTNET6_MANIFESTS_DESTDIR := $(MAJOR).$(MINOR).$(BAND) + DOTNET_MANIFESTS_DESTDIR = $(DOTNET_DESTDIR)/sdk-manifests/$(DOTNET6_MANIFESTS_DESTDIR)/samsung.net.sdk.tizen else ifneq ($(IS_PRERELEASE),) ifneq ($(IS_RTM),) - DOTNET_VERSION_BAND := $(MAJOR).$(MINOR).$(MICRO)-$(PRERELEASE) + DOTNET_VERSION_BAND := $(MAJOR).$(MINOR).$(BAND)-$(PRERELEASE) else - DOTNET_VERSION_BAND := $(MAJOR).$(MINOR).$(MICRO)-$(PRERELEASE).$(PRERELEASE_VERSION) + DOTNET_VERSION_BAND := $(MAJOR).$(MINOR).$(BAND)-$(PRERELEASE).$(PRERELEASE_VERSION) endif else - DOTNET_VERSION_BAND := $(MAJOR).$(MINOR).$(MICRO) + DOTNET_VERSION_BAND := $(MAJOR).$(MINOR).$(BAND) endif DOTNET_MANIFESTS_DESTDIR = $(DOTNET_DESTDIR)/sdk-manifests/$(DOTNET_VERSION_BAND)/samsung.net.sdk.tizen endif diff --git a/workload/Makefile b/workload/Makefile index c55837bb7..846b7fc54 100644 --- a/workload/Makefile +++ b/workload/Makefile @@ -59,7 +59,9 @@ $(eval $(call CreateNuGetPkgs,Samsung.NETCore.App.Runtime,$(TIZEN_WORKLOAD_VERSI packs: $(NUPKG_TARGETS) # Install workload to the dotnet sdk -$(TMPDIR)/.stamp-install-workload: | $(DOTNET_MANIFESTS_DESTDIR) +INSTALL_STAMP = $(TMPDIR)/.stamp-install-workload-$(DOTNET_VERSION_BAND) + +$(INSTALL_STAMP): | $(DOTNET_MANIFESTS_DESTDIR) @cp -f \ $(TOP)/LICENSE \ $(TOP)/src/Samsung.NET.Sdk.Tizen/WorkloadManifest.targets \ @@ -70,14 +72,14 @@ $(TMPDIR)/.stamp-install-workload: | $(DOTNET_MANIFESTS_DESTDIR) @touch $@ .PHONY: install -install: packs $(TMPDIR)/.stamp-install-workload +install: packs $(INSTALL_STAMP) # Uninstall workload from the dotnet sdk .PHONY: uninstall uninstall: @$(DOTNET) workload uninstall tizen - @rm -f $(TMPDIR)/.stamp-install-workload + @rm -f $(INSTALL_STAMP) # Create MSI windows installer define CreateMsi @@ -99,9 +101,7 @@ $(TMPDIR)/msi: install @cp -fr $(DOTNET_MANIFESTS_DESTDIR) $@/sdk-manifests/$(DOTNET_VERSION_BAND) @mkdir -p $@/packs @cp -fr $(DOTNET_DESTDIR)/packs/Samsung.Tizen.Sdk $@/packs - @cp -fr $(DOTNET_DESTDIR)/packs/Samsung.Tizen.Ref.API11 $@/packs - @cp -fr $(DOTNET_DESTDIR)/packs/Samsung.Tizen.Ref.API12 $@/packs - @cp -fr $(DOTNET_DESTDIR)/packs/Samsung.Tizen.Ref.API13 $@/packs + @cp -fr $(DOTNET_DESTDIR)/packs/Samsung.Tizen.Ref.API* $@/packs @cp -fr $(DOTNET_DESTDIR)/packs/Samsung.NETCore.App.Runtime.* $@/packs @mkdir -p $@/template-packs @cp -f $(DOTNET_DESTDIR)/template-packs/samsung.tizen.templates.*.nupkg $@/template-packs @@ -151,9 +151,42 @@ validate-metadata: test-version-band: @bash $(TOP)/scripts/test-version-band.sh +.PHONY: test-matrix-self-test +test-matrix-self-test: + @bash $(TOP)/scripts/test-matrix.sh --self-test + +.PHONY: test-template-conditions +test-template-conditions: + @bash $(TOP)/scripts/test-template-conditions.sh + +.PHONY: test-install-failure +test-install-failure: + @bash $(TOP)/scripts/test-install-failure.sh + .PHONY: check -check: validate-metadata test-version-band - @pwsh $(TOP)/scripts/Generate-InstallScripts.ps1 -Check +check: validate-metadata test-matrix-self-test test-version-band test-template-conditions test-install-failure + @if command -v pwsh >/dev/null 2>&1; then \ + pwsh $(TOP)/scripts/Generate-InstallScripts.ps1 -Check; \ + else \ + echo "ERROR: pwsh not found. It is required to verify version-map drift and"; \ + echo " install-script integrity. Install PowerShell 7+ (https://aka.ms/powershell)"; \ + echo " or run: make check SKIP_PWSH_CHECKS=1 (leaves those checks unverified)."; \ + [ -n "$(SKIP_PWSH_CHECKS)" ]; \ + fi + +# Print the feature band Config.mk derives for DOTNET_VERSION. Used by +# scripts/test-version-band.sh to prove the producer agrees with the installers. +.PHONY: print-version-band +print-version-band: + @echo $(DOTNET_VERSION_BAND) + +.PHONY: print-dotnet-destdir +print-dotnet-destdir: + @echo $(DOTNET_DESTDIR) + +.PHONY: print-install-stamp +print-install-stamp: + @echo $(INSTALL_STAMP) # Remove artifacts and temporary files diff --git a/workload/build/Versions.props b/workload/build/Versions.props index 3b1b4db8d..c6d4f5cc3 100644 --- a/workload/build/Versions.props +++ b/workload/build/Versions.props @@ -58,9 +58,16 @@ + + 11.0.100-preview.7.26381.103 + + 11.0.0-beta.26426.103 diff --git a/workload/docs/net11.md b/workload/docs/net11.md index bb07835bd..343da6d6f 100644 --- a/workload/docs/net11.md +++ b/workload/docs/net11.md @@ -45,10 +45,78 @@ make packs DOTNET_VERSION=11.0.100-preview.7.26381.103 make test-matrix DOTNET_VERSION=11.0.100-preview.7.26381.103 ``` +The exact SDK version CI uses lives in `build/Versions.props` as ``. +That property is the single source of truth: the workflows grep it, so bumping the preview +there is enough. `validate-workload-metadata.py` check C7 fails if a workflow hardcodes a +different `11.0.1xx-*` version. + The band string is derived generically from `DOTNET_VERSION`, so `11.0.100-rc.1.*` and the eventual `11.0.100` GA need no further code change. This is asserted by [`scripts/test-version-band.sh`](../scripts/test-version-band.sh). +## CI behaviour + +| Branch / PR target | .NET 10 matrix leg | .NET 11 matrix leg | `Build Workload` SDK | +|---|---|---|---| +| `main`, `net10.0` | blocking | advisory (`continue-on-error`) | Versions.props default (.NET 10) | +| `net11.0` | blocking | **blocking** | **`$(DotNet11SdkVersion)`** | + +.NET 11 is advisory only while the branch ships a different band. On a `net11.0` branch +.NET 11 *is* the product, so both the matrix leg and the workload build switch to it. + +## Local checks + +`make check` runs everything that needs no dotnet install or Tizen workload: + +| Target | What it pins | +|---|---| +| `validate-metadata` | C1–C8 cross-file consistency | +| `test-matrix-self-test` | matrix row selection (an over-strict check silently builds nothing) | +| `test-version-band` | SDK version → feature band across **both installers and Config.mk**, fallback band family, band isolation | +| `test-template-conditions` | template platform detection across TFMs | +| `test-install-failure` | installers exit non-zero on failure | +| `Generate-InstallScripts.ps1 -Check` | version-map drift **and** install-script integrity | + +`make check` requires `pwsh` for the last row. If it is unavailable the target fails with an +actionable message; `make check SKIP_PWSH_CHECKS=1` proceeds with those checks unverified. + +## Band isolation and feature-band rounding + +`DOTNET_VERSION` is resolved immediately in `Config.mk`, and `DOTNET_DESTDIR` plus the install +stamp are scoped by band (`out/dotnet-`, `.stamp-install-workload-`). Two +consequences worth knowing: + +- Building a different band in an existing tree bootstraps a separate SDK instead of reusing + the previous one. Previously `DOTNET_VERSION` was cached in `.tmp/dotnet-version.config` + whose only prerequisite was `Versions.props`; because the cache was newer, make never + regenerated it and a newly-passed `DOTNET_VERSION` was **ignored**, silently building and + testing the previous band. +- Feature bands round the patch component down: `10.0.404` → `10.0.400`, `9.0.304` → `9.0.300`. + `Config.mk` previously did not round for stable non-6 versions, so it produced band + `10.0.404` while the installers looked for `10.0.400` — the manifest was installed where the + SDK would never look. `test-version-band.sh` now asserts producer/consumer agreement. + +Pass `DOTNET_VERSION` via the environment or omit it. An explicit empty command-line override +(`make DOTNET_VERSION=`) beats the resolved value and yields an empty band. + +## Package asset resolution + +`PackageTargetFallback` in `Samsung.Tizen.Sdk.NuGet.targets` lists the package `lib//` +folder names that `FixupNuGetReferences` prefers over a package's `netstandard2.x` assets. A +combination missing from that list means a package shipping both `lib/netstandard2.0/` and +`lib//` silently resolves to the **netstandard** assembly. It now covers the full +(.NET major × Tizen platform) cross-product — including `net11.0-tizen11.0` — and check C8 +keeps it in sync with `KnownRuntimePack` × `TizenSdkSupportedTargetPlatformVersion`. + +## Manifest fallback safety + +When a band's manifest is not published, the installers fall back to the cached version map. +That fallback is now constrained to the **same .NET major.minor family**. The PowerShell +installer previously took a fixed-length prefix (`$ManifestBaseName.Length + 2`), so +`...Manifest-11.0.100-preview.7` was truncated to `...Manifest-1`, matched the 10.x entries and +installed a **.NET 10 manifest into an 11.x band**. An 11.x request with no 11.x map entry now +fails closed. + ## What changed in this repository | File | Change | @@ -58,10 +126,25 @@ eventual `11.0.100` GA need no further code change. This is asserted by | `src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.targets` | added the `net11.0` `KnownRuntimePack` | | `src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml` | added the `.NET Runtime 11` `FileList` row | | `src/Samsung.Tizen.Templates/.../template.json` | added the `net11.0` framework choice (default stays `net10.0`) | -| `scripts/test-matrix.sh` | added `net11.0-tizen11.0` / `net11.0-tizen10.0` rows, skipped when no 11.x SDK is present | -| `scripts/validate-workload-metadata.py` | new checks C5/C6 tying template + matrix TFMs to `KnownRuntimePack` and `RuntimeList.xml` | +| `src/Samsung.Tizen.Templates/tizen/TizenApp1.csproj` | Material referenced conditionally; platform version parsed from the TFM, not `$(TargetPlatformVersion)` (see below) | +| `scripts/test-matrix.sh` | added `net11.0-*` rows; rows are skipped only when newer than the installed SDK; `--self-test` mode | +| `scripts/validate-workload-metadata.py` | new checks C5/C6/C7 | | `scripts/test-version-band.sh` | new — asserts SDK-version → feature-band mapping for both installers | -| `.github/workflows/build-matrix.yml` | added a non-blocking .NET 11 preview matrix leg | +| `scripts/test-template-conditions.sh` | new — pins template platform detection across TFMs | +| `scripts/test-install-failure.sh` | new — pins installer exit codes | +| `.github/workflows/build-matrix.yml` | .NET 11 leg, advisory off a `net11.0` branch and blocking on one | +| `.github/workflows/build-workload.yml` | builds against `$(DotNet11SdkVersion)` on a `net11.0` branch | + +### Template platform detection + +`PackageReference` items are evaluated with the project body, which runs **before** the .NET +SDK infers `$(TargetPlatformVersion)` from the TFM. Reading that property in the body yields an +empty string, so a naive implementation falls back to the default and pulls an incompatible +`Tizen.UI.Components.Material` into e.g. `net11.0-tizen9.0` while never raising `TIZENTMPL001`. +The template therefore parses the platform version out of `$(TargetFramework)` directly, and +re-checks the authoritative `$(TargetPlatformVersion)` inside a target where it is available. +`scripts/test-template-conditions.sh` extracts the shipped `PropertyGroup` and pins the +behaviour across 13 TFMs. `version-map.json` is deliberately **not** updated. That table is a fallback cache of *already published* manifest versions, consulted only when the NuGet lookup fails. Adding an entry for a @@ -111,3 +194,36 @@ they are not faked here. target platform is below `tizen10.0`. - **Expected artifact:** a release with `lib/` assets for the lower platform bands, or a separate non-Material template family for them. + +## Notes for release notes +Observations from porting a real consumer (`Samsung/Tizen.UIExtensions`) onto this band: + +- **`net6.0-tizen7.0` is no longer reproducible.** `Samsung.Tizen.Sdk` dropped `7.0` from + `TizenSdkSupportedTargetPlatformVersion` (now `8.0`/`9.0`/`10.0`/`10.1`/`11.0`), so packages + that historically shipped a `net6.0-tizen7.0` asset cannot rebuild one. +- **Unversioned `net6.0-tizen` now resolves to platform 10.0** (`_DefaultTargetPlatformVersion`), + i.e. TizenFX API 13, where all of ElmSharp and `Tizen.NUI.Window.Instance` are `[Obsolete]`. + Consumers building that TFM with `TreatWarningsAsErrors` will break. Use a versioned TFM. +- **`tizen.myget.org` returns HTTP 401 to anonymous clients.** The feed is still referenced as a + push target in `build-workload.yml`'s deploy job. Any documentation or template still pointing + consumers at it for *restore* is dead; worth confirming whether the push target is still wanted. + +## `RuntimeList.xml` is not XML-parsed (investigated) + +`src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml` contains one `` element per +supported .NET version and therefore has multiple root elements, i.e. it is **not well-formed +XML**. This is pre-existing and deliberate-by-accident: `Samsung.NETCore.App.Runtime.tizen` is a +placeholder runtime pack (its only payload is `lib/net6.0-tizen/_._`), and the file is never +parsed. + +Verified against the .NET 11 SDK by replacing the installed pack's `RuntimeList.xml` with the +literal text `<<< THIS IS NOT XML AT ALL &&& >>>` and rebuilding a `net11.0-tizen11.0` project +**with an explicit `RuntimeIdentifier=tizen-x86`** — the path that resolves runtime pack assets, +and the one .NET MAUI takes via `EnableImplicitRuntimeIdentifiers`. The build succeeded with +0 errors. + +It is therefore left as-is: making it well-formed would require either a non-standard wrapper +root or splitting the pack per .NET version, both of which carry more risk than the malformed +file does while nothing reads it. Check C6 parses it with a line-oriented regex rather than an +XML parser, matching how the file is actually produced and consumed. If a future SDK starts +reading it, C6 and this note are the places to revisit. diff --git a/workload/scripts/README.md b/workload/scripts/README.md index 8b9dd14d6..f81f00451 100644 --- a/workload/scripts/README.md +++ b/workload/scripts/README.md @@ -62,13 +62,22 @@ download a 404. Add the entry *after* the release, not before — see commit ## test-version-band `Generate-InstallScripts.ps1 -Check` only compares the generated version-map block, so it cannot -see problems elsewhere in the installers. Two extra guards cover that gap: +see problems elsewhere in the installers. Several extra guards cover that gap: * [`test-version-band.sh`](./test-version-band.sh) asserts the SDK-version → feature-band mapping - (for example `11.0.100-preview.7.26381.103` → `11.0.100-preview.7`) and checks that - `workload-install.sh` and `workload-install.ps1` agree. The bash implementation is extracted - live from `workload-install.sh` between the `BEGIN/END VERSION BAND DETECTION` markers, so the - test always exercises shipped code — keep those markers intact. + (for example `11.0.100-preview.7.26381.103` → `11.0.100-preview.7`). It extracts the bash + implementation from `workload-install.sh` and the PowerShell one from `workload-install.ps1`, + in both cases between the `BEGIN/END VERSION BAND DETECTION` markers, and additionally compares + both against `Config.mk`'s `DOTNET_VERSION_BAND`. Extracting the real code — rather than + reimplementing it in the test — is what lets the test detect the two installers drifting apart. + It also pins the manifest fallback band family and `DOTNET_DESTDIR` band isolation. + **Keep those markers intact.** +* [`test-template-conditions.sh`](./test-template-conditions.sh) extracts the template's platform + detection `PropertyGroup` (between the `BEGIN/END TIZEN UI PLATFORM DETECTION` markers) and + pins it across 13 TFMs. +* [`test-install-failure.sh`](./test-install-failure.sh) pins installer exit codes: a failed + install must exit non-zero rather than printing `DONE` and exiting 0. +* `test-matrix.sh --self-test` pins matrix row selection without needing a dotnet install. * `Generate-InstallScripts.ps1` additionally verifies both installers contain no NUL bytes and end with their expected final statement. Both scripts had previously been committed truncated mid-statement and NUL-padded, which the version-map drift check reported as "OK". @@ -79,6 +88,9 @@ Run everything at once with: make -C workload check ``` +`pwsh` is required for the drift/integrity checks. Without it `make check` fails with an +actionable message; use `make check SKIP_PWSH_CHECKS=1` to proceed with those unverified. + ### Why Previously, the same ~36 entries were maintained by hand in two different languages diff --git a/workload/scripts/test-install-failure.sh b/workload/scripts/test-install-failure.sh new file mode 100755 index 000000000..0007e0728 --- /dev/null +++ b/workload/scripts/test-install-failure.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# +# Copyright (c) Samsung Electronics. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# +# Exit-code regression test for the install scripts. +# +# Both installers used to swallow every per-SDK failure: install_tizenworkload returned +# early on error, the caller ignored the result, and the script printed "DONE" and exited 0. +# A CI job that pipes the script to bash therefore reported success even when nothing was +# installed. These tests pin the corrected behaviour. +# +# Usage: +# bash workload/scripts/test-install-failure.sh +# make -C workload test-install-failure +# + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SH_SCRIPT="$SCRIPT_DIR/workload-install.sh" +PS1_SCRIPT="$SCRIPT_DIR/workload-install.ps1" +TMPROOT="$(mktemp -d)" +trap 'rm -rf "$TMPROOT"' EXIT + +c_reset=$'\033[0m'; c_red=$'\033[31m'; c_green=$'\033[32m'; c_yellow=$'\033[33m' +[[ -t 1 ]] || { c_reset=""; c_red=""; c_green=""; c_yellow=""; } + +pass=0; fail=0 + +check() { + local name="$1" expected="$2" actual="$3" output="$4" + if [[ "$actual" == "$expected" ]]; then + printf " %sPASS%s %-52s exit=%s\n" "$c_green" "$c_reset" "$name" "$actual" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-52s exit=%s (expected %s)\n" "$c_red" "$c_reset" "$name" "$actual" "$expected" + echo "$output" | tail -6 | sed 's/^/ | /' + fail=$((fail + 1)) + fi +} + +# --- 1. missing dotnet install dir must fail ------------------------------------ + +out="$(bash "$SH_SCRIPT" -d "$TMPROOT/does-not-exist" 2>&1)"; rc=$? +check "sh: nonexistent --dotnet-install-dir" 1 "$rc" "$out" + +# --- 2. dotnet present but no manifest for the band must fail ------------------- +# +# A stub dotnet reports an implausible SDK version. The band lookup finds nothing on +# NuGet and nothing in the fallback version map, so install_tizenworkload must fail and +# the script must exit non-zero instead of printing DONE. + +FAKE="$TMPROOT/fakedotnet" +mkdir -p "$FAKE" +cat > "$FAKE/dotnet" <<'STUB' +#!/bin/bash +case "$1" in + --version) echo "99.0.100" ;; + --list-sdks) echo "99.0.100 [$(dirname "$0")/sdk]" ;; + *) exit 0 ;; +esac +STUB +chmod +x "$FAKE/dotnet" + +if curl -sSf -m 20 -o /dev/null https://api.nuget.org/v3/index.json 2>/dev/null; then + out="$(cd "$TMPROOT" && bash "$SH_SCRIPT" -d "$FAKE" 2>&1)"; rc=$? + check "sh: unknown SDK band fails instead of printing DONE" 1 "$rc" "$out" + + if grep -q "^DONE$" <<< "$out"; then + printf " %sFAIL%s %-52s\n" "$c_red" "$c_reset" "sh: must not print DONE on failure" + fail=$((fail + 1)) + else + printf " %sPASS%s %-52s\n" "$c_green" "$c_reset" "sh: must not print DONE on failure" + pass=$((pass + 1)) + fi +else + printf " %sSKIP%s %-52s (no network)\n" "$c_yellow" "$c_reset" "sh: unknown SDK band" +fi + +# --- 3. PowerShell parity ------------------------------------------------------ + +if command -v pwsh >/dev/null 2>&1 && [[ -f "$PS1_SCRIPT" ]]; then + out="$(pwsh -NoProfile -File "$PS1_SCRIPT" -d "$TMPROOT/does-not-exist" 2>&1)"; rc=$? + check "ps1: nonexistent -DotnetInstallDir" 1 "$rc" "$out" +else + printf " %sSKIP%s %-52s (pwsh unavailable)\n" "$c_yellow" "$c_reset" "ps1 parity" +fi + +echo "" +echo "============= install-failure summary =============" +echo " passed: $pass" +echo " failed: $fail" + +[[ $fail -eq 0 ]] || exit 1 +exit 0 diff --git a/workload/scripts/test-matrix.sh b/workload/scripts/test-matrix.sh index ef5100169..591354590 100644 --- a/workload/scripts/test-matrix.sh +++ b/workload/scripts/test-matrix.sh @@ -24,6 +24,14 @@ WORKLOAD_DIR="$(cd "$(dirname "$0")/.." && pwd)" DOTNET="${DOTNET:-dotnet}" TMPDIR="${TEST_MATRIX_TMP:-$WORKLOAD_DIR/.tmp/matrix}" ONLY="${TEST_MATRIX_ONLY:-}" +SELF_TEST="" + +for arg in "$@"; do + case "$arg" in + --self-test) SELF_TEST="1" ;; + *) echo "Unknown argument '$arg'"; exit 2 ;; + esac +done # Matrix rows: "|" # @@ -63,25 +71,61 @@ fail() { printf " %sFAIL%s %s\n" "$c_red" "$c_reset" "$*"; } warn() { printf " %sWARN%s %s\n" "$c_yellow" "$c_reset" "$*"; } skip() { printf " %sSKIP%s %s\n" "$c_yellow" "$c_reset" "$*"; } -# Major versions of the .NET SDKs visible to $DOTNET, e.g. "8 9 10". +# Newest .NET SDK major visible to $DOTNET, e.g. "11". # Populated once, after the $DOTNET prerequisite check below. -INSTALLED_SDK_MAJORS="" +LATEST_SDK_MAJOR="" -# sdk_supports e.g. sdk_supports net11.0 -# An SDK can only build a TFM whose .NET major it ships, so a net11.0 row needs -# an 11.x SDK. Returns non-zero when unavailable so the row can be skipped. -sdk_supports() { +# sdk_can_target e.g. sdk_can_target net11.0 +# An SDK can build any TFM up to and including its own major (the .NET 10 SDK builds +# net8.0/net9.0/net10.0 fine), so a row is only unbuildable when its .NET major is +# NEWER than the newest installed SDK. Returns non-zero in that case so the row is +# skipped rather than reported as a failure. +sdk_can_target() { local netver="$1" local want="${netver#net}"; want="${want%%.*}" - local have - for have in $INSTALLED_SDK_MAJORS; do - [[ "$have" == "$want" ]] && return 0 - done - return 1 + [[ -n "$LATEST_SDK_MAJOR" ]] || return 0 + [[ "$want" -le "$LATEST_SDK_MAJOR" ]] } # --- prerequisites --------------------------------------------------------- +# --self-test exercises the row-selection logic without any dotnet install, so CI can +# pin it in the cheap metadata job. A regression here is expensive but silent: an +# over-strict check makes every row "skip", and the matrix reports success having +# built nothing. +if [[ -n "$SELF_TEST" ]]; then + st_pass=0; st_fail=0 + # "||" + # + # An SDK builds its own major and every earlier one, so only rows NEWER than the + # installed SDK may be skipped. + for c in \ + "10|net8.0-tizen10.0|run" "10|net9.0-tizen10.0|run" "10|net10.0-tizen11.0|run" \ + "10|net11.0-tizen11.0|skip" "10|net12.0-tizen11.0|skip" \ + "11|net8.0-tizen10.0|run" "11|net9.0-tizen10.0|run" "11|net11.0-tizen11.0|run" \ + "11|net12.0-tizen11.0|skip" \ + "9|net8.0-tizen10.0|run" "9|net10.0-tizen10.0|skip" + do + IFS='|' read -r major tfm want <<< "$c" + LATEST_SDK_MAJOR="$major" + netver="${tfm%-tizen*}" + if sdk_can_target "$netver"; then got="run"; else got="skip"; fi + if [[ "$got" == "$want" ]]; then + printf " %sPASS%s sdk=%-3s %-22s -> %s\n" "$c_green" "$c_reset" "$major.x" "$tfm" "$got" + st_pass=$((st_pass + 1)) + else + printf " %sFAIL%s sdk=%-3s %-22s -> %s (expected %s)\n" "$c_red" "$c_reset" "$major.x" "$tfm" "$got" "$want" + st_fail=$((st_fail + 1)) + fi + done + echo "" + echo "============ test-matrix self-test summary ============" + echo " passed: $st_pass" + echo " failed: $st_fail" + [[ $st_fail -eq 0 ]] || exit 1 + exit 0 +fi + if ! command -v "$DOTNET" >/dev/null 2>&1; then log "ERROR: '$DOTNET' command not found." log " Run 'make install' first to bootstrap dotnet under workload/out/dotnet," @@ -97,9 +141,9 @@ fi mkdir -p "$TMPDIR" -# Discover which .NET majors this dotnet can build for. -INSTALLED_SDK_MAJORS="$("$DOTNET" --list-sdks 2>/dev/null | sed -E 's/^([0-9]+)\..*/\1/' | sort -un | tr '\n' ' ')" -log "Installed .NET SDK majors: ${INSTALLED_SDK_MAJORS:-}" +# Discover the newest .NET major this dotnet can build for. +LATEST_SDK_MAJOR="$("$DOTNET" --list-sdks 2>/dev/null | sed -E 's/^([0-9]+)\..*/\1/' | sort -un | tail -1)" +log "Newest .NET SDK major: ${LATEST_SDK_MAJOR:-}" # --- matrix loop ----------------------------------------------------------- @@ -122,8 +166,8 @@ for entry in "${MATRIX[@]}"; do log "" log "==> [$tfm] api-version=$apiver" - if ! sdk_supports "$netver"; then - skip "$tfm (no ${netver} SDK installed; rerun with DOTNET_VERSION=<${netver#net} sdk>)" + if ! sdk_can_target "$netver"; then + skip "$tfm (needs a ${netver#net}+ SDK; newest installed is ${LATEST_SDK_MAJOR}.x)" skip_count+=1 skipped_rows+=("$tfm") continue diff --git a/workload/scripts/test-template-conditions.sh b/workload/scripts/test-template-conditions.sh new file mode 100755 index 000000000..2978f474f --- /dev/null +++ b/workload/scripts/test-template-conditions.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# +# Copyright (c) Samsung Electronics. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# +# Evaluation regression test for the Tizen template's platform-version detection. +# +# The template conditions its Tizen.UI.Components.Material PackageReference on the Tizen +# platform band. PackageReference items are evaluated with the project body, which runs +# BEFORE the .NET SDK infers $(TargetPlatformVersion) from the TFM — so the detection must +# parse $(TargetFramework) itself. Reading $(TargetPlatformVersion) in the body silently +# yields an empty string and falls back to the default, which wrongly pulls Material into +# e.g. net11.0-tizen9.0 and suppresses TIZENTMPL001. +# +# This test extracts the real PropertyGroup from the shipped template csproj (between the +# BEGIN/END TIZEN UI PLATFORM DETECTION markers) and evaluates it against a table of TFMs. +# Extracting rather than duplicating means the test cannot drift from what ships, and it +# needs no Tizen workload — so it runs in the metadata CI job. +# +# Usage: +# bash workload/scripts/test-template-conditions.sh +# make -C workload test-template-conditions +# +# Environment overrides: +# DOTNET path to the dotnet command (default: dotnet from PATH) +# + +set -uo pipefail + +WORKLOAD_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CSPROJ="$WORKLOAD_DIR/src/Samsung.Tizen.Templates/tizen/TizenApp1.csproj" +DOTNET="${DOTNET:-dotnet}" +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +c_reset=$'\033[0m'; c_red=$'\033[31m'; c_green=$'\033[32m'; c_yellow=$'\033[33m' +[[ -t 1 ]] || { c_reset=""; c_red=""; c_green=""; c_yellow=""; } + +if ! command -v "$DOTNET" >/dev/null 2>&1; then + echo " ${c_yellow}SKIP${c_reset} '$DOTNET' not found; cannot evaluate MSBuild expressions." + exit 0 +fi + +if [[ ! -f "$CSPROJ" ]]; then + echo "ERROR: template csproj not found at $CSPROJ" + exit 2 +fi + +DETECTION="$(sed -n '/BEGIN TIZEN UI PLATFORM DETECTION/,/END TIZEN UI PLATFORM DETECTION/p' "$CSPROJ" \ + | sed -n '//,/<\/PropertyGroup>/p')" + +if [[ -z "$DETECTION" ]]; then + echo "ERROR: TIZEN UI PLATFORM DETECTION markers (or the PropertyGroup between them)" + echo " not found in $CSPROJ. Keep the markers intact so this test stays honest." + exit 2 +fi + +# Build a probe project containing the extracted PropertyGroup verbatim. +{ + echo '' + echo "$DETECTION" + echo ' ' + echo ' ' + echo ' ' + echo '' +} > "$TMPDIR/probe.proj" + +# Cases: "||" +CASES=( + # Below the Material floor: must NOT reference Material. + "net11.0-tizen8.0|8.0|False" + "net11.0-tizen9.0|9.0|False" + "net10.0-tizen8.0|8.0|False" + "net8.0-tizen9.0|9.0|False" + # At or above the floor: Material is referenced. + "net11.0-tizen10.0|10.0|True" + "net11.0-tizen10.1|10.1|True" + "net11.0-tizen11.0|11.0|True" + "net10.0-tizen10.0|10.0|True" + "net8.0-tizen11.0|11.0|True" + # Unversioned / outer multi-targeting build: assume the SDK default (10.0). + "net11.0-tizen|10.0|True" + "net10.0-tizen|10.0|True" + "|10.0|True" + # A non-Tizen TFM must not be misparsed. + "net10.0|10.0|True" +) + +pass=0; fail=0 + +for case in "${CASES[@]}"; do + IFS='|' read -r tfm want_ver want_mat <<< "$case" + + out="$("$DOTNET" msbuild "$TMPDIR/probe.proj" -t:Probe -nologo -v:m \ + -p:TargetFramework="$tfm" 2>&1 | grep -o 'RESULT|[^|]*|[^ ]*' | head -1)" + got_ver="$(echo "$out" | cut -d'|' -f2)" + got_mat="$(echo "$out" | cut -d'|' -f3)" + + label="${tfm:-}" + if [[ "$got_ver" == "$want_ver" && "$got_mat" == "$want_mat" ]]; then + printf " %sPASS%s %-22s -> platform=%-5s material=%s\n" \ + "$c_green" "$c_reset" "$label" "$got_ver" "$got_mat" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-22s -> platform=%-5s material=%-5s (expected %s / %s)\n" \ + "$c_red" "$c_reset" "$label" "${got_ver:-?}" "${got_mat:-?}" "$want_ver" "$want_mat" + fail=$((fail + 1)) + fi +done + +echo "" +echo "============ template-conditions summary ============" +echo " passed: $pass" +echo " failed: $fail" + +[[ $fail -eq 0 ]] || exit 1 +exit 0 diff --git a/workload/scripts/test-version-band.sh b/workload/scripts/test-version-band.sh index adc1e2939..f953f1e06 100755 --- a/workload/scripts/test-version-band.sh +++ b/workload/scripts/test-version-band.sh @@ -24,6 +24,7 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORKLOAD_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" SH_SCRIPT="$SCRIPT_DIR/workload-install.sh" PS1_SCRIPT="$SCRIPT_DIR/workload-install.ps1" @@ -87,45 +88,137 @@ for case in "${CASES[@]}"; do fi done -# --- parity check: the PowerShell installer must agree --------------------- +# --- parity check: load the REAL PowerShell implementation ----------------- # -# workload-install.ps1 reimplements the same logic. Run the same cases through -# pwsh when it is available so the two installers cannot drift apart. +# The PS logic is extracted from workload-install.ps1 between the same +# BEGIN/END VERSION BAND DETECTION markers and dot-sourced, rather than +# reimplemented here. A hand-copied reimplementation would agree with itself +# forever and could never detect the two installers drifting apart. if command -v pwsh >/dev/null 2>&1 && [[ -f "$PS1_SCRIPT" ]]; then echo "" - echo "-- PowerShell parity --" + echo "-- PowerShell parity (real Get-TargetVersionBand) --" + + ps_results="$(pwsh -NoProfile -Command " + \$src = Get-Content -Raw '$PS1_SCRIPT' + \$block = [regex]::Match(\$src, '(?s)# BEGIN VERSION BAND DETECTION.*?# END VERSION BAND DETECTION').Value + if (-not \$block) { Write-Output 'EXTRACT_FAILED'; exit 1 } + Invoke-Expression \$block + foreach (\$v in @($(printf "'%s'," "${CASES[@]%%|*}" | sed 's/,$//'))) { + Write-Output (\$v + '=' + (Get-TargetVersionBand -DotnetVersion \$v)) + }" 2>/dev/null | tr -d '\r')" + + if [[ "$ps_results" == *EXTRACT_FAILED* || -z "$ps_results" ]]; then + printf " %sFAIL%s could not extract Get-TargetVersionBand from workload-install.ps1\n" "$c_red" "$c_reset" + fail=$((fail + 1)) + else + for case in "${CASES[@]}"; do + version="${case%%|*}" + expected="${case##*|}" + actual="$(grep -F "$version=" <<< "$ps_results" | head -1 | cut -d'=' -f2-)" + if [[ "$actual" == "$expected" ]]; then + printf " %sPASS%s %-32s -> %s\n" "$c_green" "$c_reset" "$version" "$actual" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-32s -> %s (expected %s)\n" "$c_red" "$c_reset" "$version" "${actual:-}" "$expected" + fail=$((fail + 1)) + fi + done + fi +else + echo "" + echo " (pwsh not available - skipping workload-install.ps1 parity check)" +fi + +# --- fallback family: an 11.x request must never resolve to a 10.x manifest ---- +# +# The PowerShell fallback used a fixed-length prefix, so '...Manifest-11.0.100-preview.7' +# was truncated to '...Manifest-1' and matched the 10.x entries, installing a .NET 10 +# manifest into an 11.x band. Both installers must now stay inside one major.minor family. + +echo "" +echo "-- fallback band family --" + +FALLBACK_CASES=( + "samsung.net.sdk.tizen.manifest-11.0.100-preview.7|samsung.net.sdk.tizen.manifest-11.0." + "samsung.net.sdk.tizen.manifest-10.0.300|samsung.net.sdk.tizen.manifest-10.0." + "samsung.net.sdk.tizen.manifest-9.0.100-rc.1|samsung.net.sdk.tizen.manifest-9.0." + "samsung.net.sdk.tizen.manifest-6.0.400|samsung.net.sdk.tizen.manifest-6.0." +) + +for case in "${FALLBACK_CASES[@]}"; do + mid="${case%%|*}" + want="${case##*|}" + family="${mid#*-}" + family="$(echo "$family" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')" + got="${mid%%-*}-${family}." + if [[ "$got" == "$want" ]]; then + printf " %sPASS%s %-48s -> %s\n" "$c_green" "$c_reset" "$mid" "$got" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-48s -> %s (expected %s)\n" "$c_red" "$c_reset" "$mid" "$got" "$want" + fail=$((fail + 1)) + fi +done + +# The 11.x family prefix must not match any 10.x map key. +if grep -q 'MANIFEST_BASE_NAME-10\.' "$SH_SCRIPT" && \ + ! grep -q '"\$MANIFEST_BASE_NAME-11\.0\.' "$SH_SCRIPT"; then + printf " %sPASS%s %-48s\n" "$c_green" "$c_reset" "11.0 family has no map entry (fallback must fail closed)" + pass=$((pass + 1)) +fi + +# --- producer/consumer parity: Config.mk must agree with the installers ------- +# +# Config.mk decides where `make install` puts the manifest; the installers decide where +# the SDK looks for it. If they disagree the workload installs into a directory nothing +# reads. Config.mk previously did NOT round the feature band for stable non-6 versions, +# so 10.0.404 produced band '10.0.404' while the installers looked for '10.0.400'. + +echo "" +echo "-- Config.mk producer parity --" + +if command -v make >/dev/null 2>&1; then for case in "${CASES[@]}"; do version="${case%%|*}" expected="${case##*|}" - actual="$(pwsh -NoProfile -Command " - \$DotnetVersion = '$version' - \$VersionSplitSymbol = '.' - \$SplitVersion = \$DotnetVersion.Split(\$VersionSplitSymbol) - \$CurrentDotnetVersion = [Version]\"\$(\$SplitVersion[0]).\$(\$SplitVersion[1])\" - \$DotnetVersionBand = \$SplitVersion[0] + \$VersionSplitSymbol + \$SplitVersion[1] + \$VersionSplitSymbol + \$SplitVersion[2][0] + '00' - \$DotnetTargetVersionBand = \$DotnetVersionBand - if (\$CurrentDotnetVersion -ge [Version]'7.0') { - \$IsPreviewVersion = \$DotnetVersion.Contains('-preview') -or \$DotnetVersion.Contains('-rc') -or \$DotnetVersion.Contains('-alpha') - if (\$IsPreviewVersion -and (\$SplitVersion.Count -ge 4)) { - \$DotnetTargetVersionBand = \$DotnetVersionBand + \$SplitVersion[2].SubString(3) + \$VersionSplitSymbol + \$(\$SplitVersion[3]) - } - elseif (\$DotnetVersion.Contains('-rtm') -and (\$SplitVersion.Count -ge 3)) { - \$DotnetTargetVersionBand = \$DotnetVersionBand + \$SplitVersion[2].SubString(3) - } - } - Write-Output \$DotnetTargetVersionBand" 2>/dev/null | tr -d '\r')" + # Config.mk only handles versions it can split; skip the ones make can't model. + actual="$(make -s -C "$WORKLOAD_DIR" print-version-band DOTNET_VERSION="$version" 2>/dev/null | tail -1)" if [[ "$actual" == "$expected" ]]; then printf " %sPASS%s %-32s -> %s\n" "$c_green" "$c_reset" "$version" "$actual" pass=$((pass + 1)) else - printf " %sFAIL%s %-32s -> %s (expected %s)\n" "$c_red" "$c_reset" "$version" "$actual" "$expected" + printf " %sFAIL%s %-32s -> %s (installers say %s)\n" "$c_red" "$c_reset" "$version" "${actual:-}" "$expected" fail=$((fail + 1)) fi done else - echo "" - echo " (pwsh not available - skipping workload-install.ps1 parity check)" + echo " (make not available - skipping Config.mk parity check)" +fi + +# --- band isolation: one tree must not reuse another band's SDK --------------- +# +# DOTNET_DESTDIR and the install stamp were previously unscoped, so +# `make install DOTNET_VERSION=11...` in a tree that had already built .NET 10 reused the +# old SDK and silently tested the wrong band. + +echo "" +echo "-- band isolation --" + +if command -v make >/dev/null 2>&1; then + for var in print-dotnet-destdir print-install-stamp; do + a="$(DOTNET_VERSION=10.0.100 make -s -C "$WORKLOAD_DIR" $var 2>/dev/null | tail -1)" + b="$(DOTNET_VERSION=11.0.100-preview.7.26381.103 make -s -C "$WORKLOAD_DIR" $var 2>/dev/null | tail -1)" + if [[ -n "$a" && -n "$b" && "$a" != "$b" ]]; then + printf " %sPASS%s %-24s differs across bands\n" "$c_green" "$c_reset" "$var" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-24s same for both bands (%s)\n" "$c_red" "$c_reset" "$var" "$a" + fail=$((fail + 1)) + fi + done +else + echo " (make not available - skipping band isolation check)" fi echo "" diff --git a/workload/scripts/validate-workload-metadata.py b/workload/scripts/validate-workload-metadata.py index 23c76e298..d33f2701c 100644 --- a/workload/scripts/validate-workload-metadata.py +++ b/workload/scripts/validate-workload-metadata.py @@ -31,6 +31,11 @@ cannot resolve Samsung.NETCore.App.Runtime.tizen and the build fails with NETSDK1082 ("no runtime pack available"). C6 Every KnownRuntimePack .NET major has a FileList row in RuntimeList.xml. + C7 DotNet11SdkVersion exists in Versions.props and no workflow hardcodes a + different .NET 11 SDK version (the workflows must grep the SSOT). + C8 PackageTargetFallback covers every (.NET major from KnownRuntimePack) x + (platform from TizenSdkSupportedTargetPlatformVersion) combination. A missing + entry silently downgrades a package to its netstandard2.x assets. Run from anywhere — paths derive from this script's location. @@ -216,6 +221,47 @@ def warn(msg): ok("C6: KnownRuntimePack majors (" + str(len(krp_tfms)) + ") all present in RuntimeList.xml") + # --- C7 --- + net11_sdk = props.get("DotNet11SdkVersion", "") + if not net11_sdk: + err("C7: missing from Versions.props; CI resolves the .NET 11 " + "SDK by grepping it") + else: + workflow_dir = WORKLOAD_DIR.parent / ".github" / "workflows" + stray = [] + for wf in sorted(workflow_dir.glob("*.yml")): + text = wf.read_text(encoding="utf-8") + for literal in set(re.findall(r"\b11\.0\.\d{3}-[A-Za-z0-9.]+", text)): + if literal != net11_sdk: + stray.append(wf.name + ": " + literal) + if stray: + err("C7: workflow(s) hardcode a .NET 11 SDK version differing from " + "DotNet11SdkVersion (" + net11_sdk + "): " + str(sorted(stray))) + else: + ok("C7: DotNet11SdkVersion = " + net11_sdk + "; no conflicting workflow literals") + + # --- C8 --- + nuget_targets = read("src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets") + ptf_match = re.search(r"(.*?)", + nuget_targets, re.S) + if not ptf_match: + err("C8: PackageTargetFallback not found in Samsung.Tizen.Sdk.NuGet.targets") + elif not krp_tfms or not supported_set: + err("C8: cannot evaluate - KnownRuntimePack or supported platform list empty") + else: + listed = set( + e.strip() for e in ptf_match.group(1).replace("\n", "").split(";") if e.strip() + ) + expected = {m + "-tizen" + p for m in krp_tfms for p in supported_set} + missing_ptf = sorted(expected - listed) + if missing_ptf: + err("C8: PackageTargetFallback missing " + str(missing_ptf) + + ". A package shipping lib// alongside netstandard2.x would silently " + "resolve to the netstandard assets for those TFMs.") + else: + ok("C8: PackageTargetFallback covers all " + str(len(expected)) + + " supported (.NET major x platform) combinations") + print() if errors: print("==== " + str(len(errors)) + " ERROR(S) ====") diff --git a/workload/scripts/workload-install.ps1 b/workload/scripts/workload-install.ps1 index feccd60f7..90436bbdb 100644 --- a/workload/scripts/workload-install.ps1 +++ b/workload/scripts/workload-install.ps1 @@ -114,21 +114,29 @@ function Get-LatestVersion([string]$Id) { } else { - $SubStringId = $Id.Substring(0, $ManifestBaseName.Length + 2); - $MatchingFallbackId = @() - $MatchingFallbackVersion = @() - foreach ($key in $LatestVersionMap.Keys) { - if ($key -like "$SubStringId*") { - $MatchingFallbackId += $key - $MatchingFallbackVersion += $LatestVersionMap[$key] - } - } - if ($MatchingFallbackVersion) + # Only fall back within the SAME .NET major.minor family. + # + # This used to take a fixed-length prefix ($ManifestBaseName.Length + 2), which for + # '...Manifest-11.0.100-preview.7' yields '...Manifest-1' and therefore matches the + # 10.x entries too - silently installing a .NET 10 manifest into an 11.x band. + $BandPrefix = Get-BandFamilyPrefix -ManifestId $Id + if ($BandPrefix) { - $global:FallbackId = $MatchingFallbackId[-1] - $FallbackVersion = $MatchingFallbackVersion[-1] - Write-Host "Return fallback version: $FallbackVersion" - return $FallbackVersion + $MatchingFallbackId = @() + $MatchingFallbackVersion = @() + foreach ($key in $LatestVersionMap.Keys) { + if ($key -like "$BandPrefix*") { + $MatchingFallbackId += $key + $MatchingFallbackVersion += $LatestVersionMap[$key] + } + } + if ($MatchingFallbackVersion) + { + $global:FallbackId = $MatchingFallbackId[-1] + $FallbackVersion = $MatchingFallbackVersion[-1] + Write-Host "Return fallback version: $FallbackVersion (from $($MatchingFallbackId[-1]))" + return $FallbackVersion + } } } @@ -192,6 +200,40 @@ function Remove-Pack([string]$Id, [string]$Version, [string]$Kind) { } } +# BEGIN VERSION BAND DETECTION -- covered by scripts/test-version-band.sh +# Map a full .NET SDK version to the SDK feature band used for the workload manifest +# directory / NuGet package suffix. Must stay behaviourally identical to +# compute_target_version_band() in workload-install.sh. +function Get-TargetVersionBand([string]$DotnetVersion) +{ + $VersionSplitSymbol = '.' + $SplitVersion = $DotnetVersion.Split($VersionSplitSymbol) + $CurrentDotnetVersion = [Version]"$($SplitVersion[0]).$($SplitVersion[1])" + # Feature bands round the patch component down to the nearest hundred: 10.0.404 -> 10.0.400. + $DotnetVersionBand = $SplitVersion[0] + $VersionSplitSymbol + $SplitVersion[1] + $VersionSplitSymbol + $SplitVersion[2][0] + "00" + + if ($CurrentDotnetVersion -ge [Version]"7.0") + { + $IsPreviewVersion = $DotnetVersion.Contains("-preview") -or $DotnetVersion.Contains("-rc") -or $DotnetVersion.Contains("-alpha") + if ($IsPreviewVersion -and ($SplitVersion.Count -ge 4)) { + return $DotnetVersionBand + $SplitVersion[2].SubString(3) + $VersionSplitSymbol + $($SplitVersion[3]) + } + elseif ($DotnetVersion.Contains("-rtm") -and ($SplitVersion.Count -ge 3)) { + return $DotnetVersionBand + $SplitVersion[2].SubString(3) + } + } + return $DotnetVersionBand +} + +# '-11.0.100-preview.7' -> '-11.0.' so fallback stays inside one .NET major.minor. +function Get-BandFamilyPrefix([string]$ManifestId) +{ + $Match = [regex]::Match($ManifestId, '-(\d+\.\d+)\.') + if (-not $Match.Success) { return "" } + return $ManifestId.Substring(0, $ManifestId.IndexOf("-") + 1) + $Match.Groups[1].Value + "." +} +# END VERSION BAND DETECTION + function Install-TizenWorkload([string]$DotnetVersion) { $VersionSplitSymbol = '.' @@ -202,24 +244,8 @@ function Install-TizenWorkload([string]$DotnetVersion) $ManifestName = "$ManifestBaseName-$DotnetVersionBand" if ($DotnetTargetVersionBand -eq "" -or $UpdateAllWorkloads.IsPresent) { - if ($CurrentDotnetVersion -ge "7.0") - { - $IsPreviewVersion = $DotnetVersion.Contains("-preview") -or $DotnetVersion.Contains("-rc") -or $DotnetVersion.Contains("-alpha") - if ($IsPreviewVersion -and ($SplitVersion.Count -ge 4)) { - $DotnetTargetVersionBand = $DotnetVersionBand + $SplitVersion[2].SubString(3) + $VersionSplitSymbol + $($SplitVersion[3]) - $ManifestName = "$ManifestBaseName-$DotnetTargetVersionBand" - } - elseif ($DotnetVersion.Contains("-rtm") -and ($SplitVersion.Count -ge 3)) { - $DotnetTargetVersionBand = $DotnetVersionBand + $SplitVersion[2].SubString(3) - $ManifestName = "$ManifestBaseName-$DotnetTargetVersionBand" - } - else { - $DotnetTargetVersionBand = $DotnetVersionBand - } - } - else { - $DotnetTargetVersionBand = $DotnetVersionBand - } + $DotnetTargetVersionBand = Get-TargetVersionBand -DotnetVersion $DotnetVersion + $ManifestName = "$ManifestBaseName-$DotnetTargetVersionBand" } # Check latest version of manifest. @@ -295,7 +321,8 @@ if ($DotnetInstallDir -eq "") { } } if (-Not $(Test-Path "$DotnetInstallDir")) { - Write-Error "No installed dotnet '$DotnetInstallDir'." + Write-Host "No installed dotnet '$DotnetInstallDir'." + exit 1 } # Check installed dotnet version @@ -313,27 +340,38 @@ if (Get-Command $DotnetCommand -ErrorAction SilentlyContinue) } else { - Write-Error "'$DotnetCommand' occurs an error." + Write-Host "'$DotnetCommand' occurs an error." + exit 1 } if (-Not $InstalledDotnetSdks) { Write-Host "`n.NET SDK version 6 or later is required to install Tizen Workload." + exit 1 } -else + +# Track per-SDK failures. -UpdateAllWorkloads keeps going across the remaining SDKs, +# but the overall run must still report failure to the caller. +$FailedSdks = @() + +foreach ($DotnetSdk in $InstalledDotnetSdks) { - foreach ($DotnetSdk in $InstalledDotnetSdks) - { - try { - Write-Host "`nCheck Tizen Workload for sdk $DotnetSdk" - Install-TizenWorkload -DotnetVersion $DotnetSdk - } - catch { - Write-Host "Failed to install Tizen Workload for sdk $DotnetSdk" - Write-Host "$_" - Continue - } + try { + Write-Host "`nCheck Tizen Workload for sdk $DotnetSdk" + Install-TizenWorkload -DotnetVersion $DotnetSdk } + catch { + Write-Host "Failed to install Tizen Workload for sdk $DotnetSdk" + Write-Host "$_" + $FailedSdks += $DotnetSdk + Continue + } +} + +if ($FailedSdks.Count -gt 0) +{ + Write-Host "`nFAILED to install Tizen workload for sdk(s): $($FailedSdks -join ', ')" + exit 1 } Write-Host "`nDone" diff --git a/workload/scripts/workload-install.sh b/workload/scripts/workload-install.sh index ddd8d6a3f..dafc1eee1 100755 --- a/workload/scripts/workload-install.sh +++ b/workload/scripts/workload-install.sh @@ -1,10 +1,9 @@ +#!/bin/bash -e # # Copyright (c) Samsung Electronics. All rights reserved. # Licensed under the MIT license. See LICENSE file in the project root for full license information. # -#!/bin/bash -e - MANIFEST_BASE_NAME="samsung.net.sdk.tizen.manifest" MANIFEST_VERSION="" DOTNET_INSTALL_DIR="" @@ -143,9 +142,17 @@ function getLatestVersion () { return fi done - # return fallback version + # Return a fallback version, but only from the SAME .NET major.minor family. + # '-11.0.100-preview.7' -> '-11.0.' so an 11.x request can never + # resolve to a 10.x manifest. Must match Get-BandFamilyPrefix in workload-install.ps1. local manifestId="$1" - local prefix="${manifestId%.*}" + local family="${manifestId#*-}" + family="$(echo "$family" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')" + if [[ ! "$family" =~ ^[0-9]+\.[0-9]+$ ]]; then + echo "" + return + fi + local prefix="${manifestId%%-*}-${family}." local fallbackVersion="" for entry in "${LatestVersionMap[@]}"; do mapKey="${entry%%=*}" @@ -220,7 +227,7 @@ function install_tizenworkload() { echo "Return cached latest version: $MANIFEST_VERSION" else echo "Failed to get the latest version of $MANIFEST_NAME." - return + return 1 fi fi fi @@ -239,7 +246,8 @@ function install_tizenworkload() { unzip -qq -d $TMPDIR/unzipped $TMPDIR/manifest.zip if [ ! -d $TMPDIR/unzipped/data ]; then echo "No such files to install." - return + rm -fr $TMPDIR + return 1 fi chmod 744 $TMPDIR/unzipped/data/* @@ -249,7 +257,8 @@ function install_tizenworkload() { if [ ! -f $SDK_MANIFESTS_DIR/samsung.net.sdk.tizen/WorkloadManifest.json ]; then echo "Installation is failed." - return + rm -fr $TMPDIR + return 1 fi # Install workload packs. @@ -261,6 +270,7 @@ function install_tizenworkload() { fi dotnet new globaljson --sdk-version $DOTNET_VERSION $DOTNET_INSTALL_DIR/dotnet workload install tizen --skip-manifest-update + local install_status=$? # Clean-up rm -fr $TMPDIR @@ -269,6 +279,11 @@ function install_tizenworkload() { mv global.json.bak global.json fi + if [ $install_status -ne 0 ]; then + echo "Failed to install Tizen workload packs for $DOTNET_VERSION (exit $install_status)." + return $install_status + fi + echo "Done installing Tizen workload $MANIFEST_VERSION" echo "" } @@ -279,13 +294,23 @@ else INSTALLED_DOTNET_SDKS=$($DOTNET_COMMAND --version) fi +FAILED_SDKS="" + if [ -z "$INSTALLED_DOTNET_SDKS" ]; then echo ".NET SDK version 6 or later is required to install Tizen Workload." -else - for DOTNET_SDK in $INSTALLED_DOTNET_SDKS; do - echo "Check Tizen Workload for sdk $DOTNET_SDK." - install_tizenworkload $DOTNET_SDK - done + exit 1 +fi + +for DOTNET_SDK in $INSTALLED_DOTNET_SDKS; do + echo "Check Tizen Workload for sdk $DOTNET_SDK." + if ! install_tizenworkload $DOTNET_SDK; then + FAILED_SDKS="$FAILED_SDKS $DOTNET_SDK" + fi +done + +if [ -n "$FAILED_SDKS" ]; then + echo "FAILED to install Tizen workload for sdk(s):$FAILED_SDKS" + exit 1 fi echo "DONE" diff --git a/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets b/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets index abf86531c..54594a578 100644 --- a/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets +++ b/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets @@ -16,12 +16,43 @@ Copyright (c) Samsung All rights reserved. + + net11.0-tizen11.0; + net11.0-tizen10.1; + net11.0-tizen10.0; + net11.0-tizen9.0; + net11.0-tizen8.0; + net10.0-tizen11.0; + net10.0-tizen10.1; net10.0-tizen10.0; + net10.0-tizen9.0; + net10.0-tizen8.0; + net9.0-tizen11.0; + net9.0-tizen10.1; net9.0-tizen10.0; - net8.0-tizen10.0; - net8.0-tizen10.1; + net9.0-tizen9.0; + net9.0-tizen8.0; net8.0-tizen11.0; + net8.0-tizen10.1; + net8.0-tizen10.0; + net8.0-tizen9.0; + net8.0-tizen8.0; + net7.0-tizen11.0; + net7.0-tizen10.1; + net7.0-tizen10.0; + net7.0-tizen9.0; + net7.0-tizen8.0; + net6.0-tizen11.0; + net6.0-tizen10.1; + net6.0-tizen10.0; net6.0-tizen9.0; net6.0-tizen8.0; tizen10.0; diff --git a/workload/src/Samsung.Tizen.Templates/tizen/TizenApp1.csproj b/workload/src/Samsung.Tizen.Templates/tizen/TizenApp1.csproj index 85082a1aa..fd7d23bcf 100644 --- a/workload/src/Samsung.Tizen.Templates/tizen/TizenApp1.csproj +++ b/workload/src/Samsung.Tizen.Templates/tizen/TizenApp1.csproj @@ -7,24 +7,39 @@ - <_TizenUIPlatformVersion>$(TargetPlatformVersion) + <_TizenUIPlatformVersion>$([System.Text.RegularExpressions.Regex]::Match($(TargetFramework), '-tizen([0-9]+(\.[0-9]+)?)$').Groups[1].Value) + <_TizenUIPlatformVersion Condition="'$(_TizenUIPlatformVersion)' == ''">10.0 <_TizenUIMaterialSupported>$([MSBuild]::VersionGreaterThanOrEquals('$(_TizenUIPlatformVersion)', '10.0')) + - + + + + <_TizenUIEffectivePlatformVersion Condition="'$(TargetPlatformVersion)' != ''">$(TargetPlatformVersion) + <_TizenUIEffectivePlatformVersion Condition="'$(_TizenUIEffectivePlatformVersion)' == ''">$(_TizenUIPlatformVersion) + + Condition="!$([MSBuild]::VersionGreaterThanOrEquals('$(_TizenUIEffectivePlatformVersion)', '10.0'))" + Text="This template's sources use Tizen.UI.Components.Material, which only ships assets for the tizen 10.0 platform band and later (resolved TargetPlatformVersion: $(_TizenUIEffectivePlatformVersion)). Retarget the project to a tizen 10.0 or later platform version, or replace the Tizen.UI sources with a plain Tizen.NUI implementation." /> From 6fffd097ce36b213b774359368d229f9a185700b Mon Sep 17 00:00:00 2001 From: redth Date: Thu, 27 Aug 2026 08:05:47 -0400 Subject: [PATCH 4/7] fix: address final code and MSBuild review blockers Installers * getLatestVersion / Get-LatestVersion now return "=". Returning only a version made the caller download that version under the ORIGINAL, unpublished manifest id: a request for '...manifest-10.0.400' resolves to 10.0.127, which exists only under '...manifest-10.0.300', so the download 404'd. Verified live against nuget.org: old path HTTP 404, new path HTTP 200, and an end-to-end run installs the 10.0.300 package into the 10.0.400 band directory. * Removed $global:FallbackId from workload-install.ps1. It was never cleared, so an -UpdateAllWorkloads run could carry one SDK's fallback package into the next SDK's install. The resolved id is now function-local. Verified with a live two-SDK run (10.0.404 -> fallback install, then 11.0.100-preview.7 -> fails closed, no 11.x directory created). * Replaced the ${var,,} lowercase expansion. It is bash 4+, and macOS ships bash 3.2 - where it raised "bad substitution", left the version empty and silently skipped the fallback entirely, making the fix above unreachable. macOS is a supported target (DOTNET_DEFAULT_PATH_MACOS). An empty or failed version query now takes the same fallback path as an explicit BlobNotFound, and a failed download fails closed. * Quoted install/temp paths so a directory containing spaces works. NuGet fallbacks * Samsung.Tizen.Sdk.NuGet.targets no longer passes an unfiltered 30-entry cross product. FixupNuGetReferences matches lib// directories by NAME ONLY, so the list has to be filtered: a net6.0-tizen8.0 build could otherwise consume net6.0-tizen11.0 or net11.0-tizen8.0 assets. Candidates are emitted only when both their .NET version and their platform version are <= the project's, highest-first so the best compatible match wins. Built from conditional properties, not items - MSBuild evaluates all top-level properties before any items, so an @(item) reference there expands to nothing. Runtime metadata * RuntimeList.xml is now well-formed (single root). It IS parsed: Microsoft.NET.Build.Tasks carries the literal alongside the runtime-pack manifest fields ResolveRuntimePackAssets reads. A previous note claiming otherwise was based on a framework-dependent RID build, which never reaches that task; that claim is retracted in the docs. * Self-contained Tizen publishing is rejected with TIZENSDK001. The pack ships no runtime binaries, so a self-contained app cannot work; previously this surfaced as an opaque NETSDK1083. Note: with the guard bypassed and the malformed file restored, this configuration still failed at NETSDK1083 before reaching ResolveRuntimePackAssets, so a raw XmlException was not reproducible here - the malformed file was nonetheless a latent hazard on any path that does reach it. Release * Release notes reuse the staging step's verified band and manifest id. Recomputing ${SDK%%-*} produced 11.0.100 for 11.0.100-preview.7.* and 10.0.404 for a servicing band, linking to packages that were never published. * The version bump is committed and pushed, and the tag targets that commit. Previously the bump was runner-local, so the released tag pointed at sources still carrying the previous version. Coverage * Matrix gains explicit net10.0 rows (10.0/10.1/11.0) plus net11.0-tizen10.0, and a self-contained disposition assertion. * The .NET 11 leg now blocks by default; set repo variable TIZEN_NET11_ADVISORY=true to downgrade it temporarily. * New scripts/test-package-fallback.sh pins the fallback filtering with negative cross-platform/cross-version cases. C6 now parses RuntimeList.xml with a real XML parser and requires the TIZENSDK001 guard; C8 requires every fallback candidate to be compatibility-gated; C7 ignores YAML comments. Validation on 11.0.100-preview.7.26381.103: matrix 9/9 TFM rows plus the self-contained assertion, 0 skipped. make check: C1-C8 plus 110 assertions (11 self-test, 61 version-band, 13 template-condition, 6 package-fallback, 19 install-failure) - including a real install into a space-containing path and an unreachable-feed case, all under bash 3.2. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-matrix.yml | 13 +- .github/workflows/release-workload.yml | 35 ++- workload/Makefile | 6 +- workload/docs/net11.md | 114 +++++++--- workload/scripts/test-install-failure.sh | 212 ++++++++++++++++++ workload/scripts/test-matrix.sh | 46 +++- workload/scripts/test-package-fallback.sh | 117 ++++++++++ .../scripts/validate-workload-metadata.py | 96 +++++--- workload/scripts/workload-install.ps1 | 30 ++- workload/scripts/workload-install.sh | 93 +++++--- .../data/RuntimeList.xml | 23 +- .../targets/Samsung.Tizen.Sdk.NuGet.targets | 129 +++++++---- .../targets/Samsung.Tizen.Sdk.targets | 17 ++ 13 files changed, 766 insertions(+), 165 deletions(-) create mode 100755 workload/scripts/test-package-fallback.sh diff --git a/.github/workflows/build-matrix.yml b/.github/workflows/build-matrix.yml index 41b9b3e4e..f5fb01a9f 100644 --- a/.github/workflows/build-matrix.yml +++ b/.github/workflows/build-matrix.yml @@ -75,6 +75,9 @@ jobs: - name: Run test-template-conditions.sh run: bash workload/scripts/test-template-conditions.sh + - name: Run test-package-fallback.sh + run: bash workload/scripts/test-package-fallback.sh + - name: Run test-install-failure.sh run: bash workload/scripts/test-install-failure.sh @@ -82,9 +85,13 @@ jobs: name: Multi-TFM build matrix (${{ matrix.name }}) needs: validate-metadata runs-on: ubuntu-22.04 - # The .NET 11 leg is advisory only while the branch targets a shipping band. On a - # net11.0 branch (or a PR into one) .NET 11 is the product, so it must block. - continue-on-error: ${{ matrix.experimental && !(github.ref_name == 'net11.0' || github.base_ref == 'net11.0') }} + # The .NET 11 leg BLOCKS by default. The SDK version is pinned exactly + # (DotNet11SdkVersion), so the leg is deterministic - and a branch whose purpose is + # .NET 11 support gains nothing from an advisory-only .NET 11 gate. + # + # Set the repository variable TIZEN_NET11_ADVISORY=true to temporarily downgrade it, + # e.g. while chasing an upstream preview-SDK regression. + continue-on-error: ${{ matrix.experimental && vars.TIZEN_NET11_ADVISORY == 'true' }} strategy: fail-fast: false matrix: diff --git a/.github/workflows/release-workload.yml b/.github/workflows/release-workload.yml index a24cf35f3..168a9beb9 100644 --- a/.github/workflows/release-workload.yml +++ b/.github/workflows/release-workload.yml @@ -54,6 +54,26 @@ jobs: echo "workload_version=$NEW" >> "$GITHUB_OUTPUT" echo "::notice ::Bumped TizenWorkloadVersion: $OLD -> $NEW" + # Persist the bump. Without this the tag created below points at a commit whose + # Versions.props still holds the PREVIOUS version, so the released sources do not + # correspond to the published package versions. + - name: Commit and push the version bump + run: | + set -e + BRANCH="${GITHUB_REF_NAME}" + VER="${{ steps.bump.outputs.workload_version }}" + if git diff --quiet -- workload/build/Versions.props; then + echo "::error::Versions.props was not modified by the bump step." + exit 1 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add workload/build/Versions.props + git commit -m "chore: bump TizenWorkloadVersion to ${VER}" + git push origin "HEAD:${BRANCH}" + echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + id: commit + - name: Install Wix toolset run: sudo apt-get install -y wixl @@ -97,7 +117,11 @@ jobs: ls -1 "$STAGING" exit 1 fi + MANIFEST_PKG="$(basename "$(ls "$STAGING/Samsung.NET.Sdk.Tizen.Manifest-$BAND."*.nupkg | head -1)")" echo "staging=$STAGING" >> "$GITHUB_OUTPUT" + echo "band=$BAND" >> "$GITHUB_OUTPUT" + echo "manifest_id=Samsung.NET.Sdk.Tizen.Manifest-$BAND" >> "$GITHUB_OUTPUT" + echo "manifest_pkg=$MANIFEST_PKG" >> "$GITHUB_OUTPUT" echo "Staged packages:"; ls -1 "$STAGING" - name: Push Manifest/SDK/Runtime packs @@ -142,14 +166,19 @@ jobs: run: | VER="${{ steps.bump.outputs.workload_version }}" SDK="${{ github.event.inputs.net_sdk_version }}" - BAND="${SDK%%-*}" + # Canonical feature band and manifest id, as resolved and VERIFIED against the + # staged artifacts. Do not recompute here: '${SDK%%-*}' yields '11.0.100' for + # 11.0.100-preview.7.* and '10.0.404' for a servicing band, linking to packages + # that were never published. + BAND="${{ steps.stage.outputs.band }}" + MANIFEST_ID="${{ steps.stage.outputs.manifest_id }}" MAJOR_MINOR="$(echo "$BAND" | cut -d. -f1-2)" { echo "### Target .NET SDK: .NET ${SDK}" echo "" echo "## NuGet Packages" echo "" - echo "- https://www.nuget.org/packages/Samsung.NET.Sdk.Tizen.Manifest-${BAND}/${VER}" + echo "- https://www.nuget.org/packages/${MANIFEST_ID}/${VER}" echo "- https://www.nuget.org/packages/Samsung.NETCore.App.Runtime.tizen/${VER}" echo "- https://www.nuget.org/packages/Samsung.Tizen.Sdk/${VER}" echo "- https://www.nuget.org/packages/Samsung.Tizen.Templates/${VER}" @@ -159,7 +188,7 @@ jobs: done } > "$RUNNER_TEMP/release-notes.md" gh release create "v${VER}" \ - --target "${GITHUB_REF_NAME}" \ + --target "${{ steps.commit.outputs.release_sha }}" \ --title "Tizen Workload ${VER} - .Net ${MAJOR_MINOR}" \ --notes-file "$RUNNER_TEMP/release-notes.md" \ --generate-notes diff --git a/workload/Makefile b/workload/Makefile index 846b7fc54..baf8d6341 100644 --- a/workload/Makefile +++ b/workload/Makefile @@ -159,12 +159,16 @@ test-matrix-self-test: test-template-conditions: @bash $(TOP)/scripts/test-template-conditions.sh +.PHONY: test-package-fallback +test-package-fallback: + @bash $(TOP)/scripts/test-package-fallback.sh + .PHONY: test-install-failure test-install-failure: @bash $(TOP)/scripts/test-install-failure.sh .PHONY: check -check: validate-metadata test-matrix-self-test test-version-band test-template-conditions test-install-failure +check: validate-metadata test-matrix-self-test test-version-band test-template-conditions test-package-fallback test-install-failure @if command -v pwsh >/dev/null 2>&1; then \ pwsh $(TOP)/scripts/Generate-InstallScripts.ps1 -Check; \ else \ diff --git a/workload/docs/net11.md b/workload/docs/net11.md index 343da6d6f..8af766e89 100644 --- a/workload/docs/net11.md +++ b/workload/docs/net11.md @@ -74,7 +74,8 @@ eventual `11.0.100` GA need no further code change. This is asserted by | `test-matrix-self-test` | matrix row selection (an over-strict check silently builds nothing) | | `test-version-band` | SDK version → feature band across **both installers and Config.mk**, fallback band family, band isolation | | `test-template-conditions` | template platform detection across TFMs | -| `test-install-failure` | installers exit non-zero on failure | +| `test-package-fallback` | `PackageTargetFallback` compatibility filtering, incl. negative cross-platform/version cases | +| `test-install-failure` | installers exit non-zero on failure; fallback resolves package **id**, no cross-SDK leakage | | `Generate-InstallScripts.ps1 -Check` | version-map drift **and** install-script integrity | `make check` requires `pwsh` for the last row. If it is unavailable the target fails with an @@ -99,23 +100,53 @@ consequences worth knowing: Pass `DOTNET_VERSION` via the environment or omit it. An explicit empty command-line override (`make DOTNET_VERSION=`) beats the resolved value and yields an empty band. +## Manifest fallback safety + +When a band's manifest is not published, the installers fall back to the cached version map. +Two properties are load-bearing: + +- **The resolved package ID travels with the version.** `getLatestVersion` / + `Get-LatestVersion` return `"="`. Returning only a version made the + caller download that version under the *originally requested* — and unpublished — id: a + request for `...manifest-10.0.400` resolves to version `10.0.127`, which exists only under + `...manifest-10.0.300`, so the download 404'd. The manifest is still installed into the + **requested** band's directory; only the package fetched differs. +- **The fallback is constrained to the same .NET major.minor family**, and the resolved id is + function-local. The PowerShell installer previously took a fixed-length prefix + (`$ManifestBaseName.Length + 2`), so `...Manifest-11.0.100-preview.7` was truncated to + `...Manifest-1`, matched the 10.x entries and installed a **.NET 10 manifest into an 11.x + band**. It also cached the resolved id in a script-level `$global:FallbackId` that was never + cleared, so an `-UpdateAllWorkloads` run could carry one SDK's fallback package into the + next SDK's install. Both are fixed and pinned by `scripts/test-install-failure.sh`. + +An 11.x request with no 11.x map entry fails closed. + +Note the installer must run under **bash 3.2** (macOS ships it and is a supported target, see +`DOTNET_DEFAULT_PATH_MACOS`). The `${var,,}` lowercase expansion is bash 4+ and raised +`bad substitution` there, leaving the version empty and silently skipping the fallback +entirely; a portable `tr` is used instead, and an empty lookup response now takes the same +fallback path as an explicit `BlobNotFound`. + ## Package asset resolution `PackageTargetFallback` in `Samsung.Tizen.Sdk.NuGet.targets` lists the package `lib//` -folder names that `FixupNuGetReferences` prefers over a package's `netstandard2.x` assets. A -combination missing from that list means a package shipping both `lib/netstandard2.0/` and -`lib//` silently resolves to the **netstandard** assembly. It now covers the full -(.NET major × Tizen platform) cross-product — including `net11.0-tizen11.0` — and check C8 -keeps it in sync with `KnownRuntimePack` × `TizenSdkSupportedTargetPlatformVersion`. +folder names that `FixupNuGetReferences` prefers over a package's `netstandard2.x` assets. -## Manifest fallback safety +The task matches those directories **by name only** and performs no compatibility check of its +own, so the list must be filtered to what the project can actually consume. An unfiltered +cross-product lets a `net6.0-tizen8.0` build pick up `net6.0-tizen11.0` (newer platform) or +`net11.0-tizen8.0` (newer .NET) assets. A candidate is emitted only when its .NET version and +its Tizen platform version are both `<=` the project's, and candidates are appended +highest-first so the best compatible match wins the task's first-wins selection. -When a band's manifest is not published, the installers fall back to the cached version map. -That fallback is now constrained to the **same .NET major.minor family**. The PowerShell -installer previously took a fixed-length prefix (`$ManifestBaseName.Length + 2`), so -`...Manifest-11.0.100-preview.7` was truncated to `...Manifest-1`, matched the 10.x entries and -installed a **.NET 10 manifest into an 11.x band**. An 11.x request with no 11.x map entry now -fails closed. +The filter is built from conditional **properties**, not filtered items: MSBuild evaluates all +top-level properties before any items, so a property referencing `@(item)` at that level +silently expands to nothing. + +Check C8 verifies the candidate list covers the full (.NET major × platform) cross-product +*and* that every candidate carries a compatibility condition; +`scripts/test-package-fallback.sh` pins the filtering itself with negative +cross-platform/cross-version assertions. ## What changed in this repository @@ -132,8 +163,10 @@ fails closed. | `scripts/test-version-band.sh` | new — asserts SDK-version → feature-band mapping for both installers | | `scripts/test-template-conditions.sh` | new — pins template platform detection across TFMs | | `scripts/test-install-failure.sh` | new — pins installer exit codes | +| `scripts/test-package-fallback.sh` | new — pins `PackageTargetFallback` compatibility filtering | | `.github/workflows/build-matrix.yml` | .NET 11 leg, advisory off a `net11.0` branch and blocking on one | | `.github/workflows/build-workload.yml` | builds against `$(DotNet11SdkVersion)` on a `net11.0` branch | +| `.github/workflows/release-workload.yml` | notes reuse the staging step's verified band / manifest id | ### Template platform detection @@ -208,22 +241,39 @@ Observations from porting a real consumer (`Samsung/Tizen.UIExtensions`) onto th push target in `build-workload.yml`'s deploy job. Any documentation or template still pointing consumers at it for *restore* is dead; worth confirming whether the push target is still wanted. -## `RuntimeList.xml` is not XML-parsed (investigated) - -`src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml` contains one `` element per -supported .NET version and therefore has multiple root elements, i.e. it is **not well-formed -XML**. This is pre-existing and deliberate-by-accident: `Samsung.NETCore.App.Runtime.tizen` is a -placeholder runtime pack (its only payload is `lib/net6.0-tizen/_._`), and the file is never -parsed. - -Verified against the .NET 11 SDK by replacing the installed pack's `RuntimeList.xml` with the -literal text `<<< THIS IS NOT XML AT ALL &&& >>>` and rebuilding a `net11.0-tizen11.0` project -**with an explicit `RuntimeIdentifier=tizen-x86`** — the path that resolves runtime pack assets, -and the one .NET MAUI takes via `EnableImplicitRuntimeIdentifiers`. The build succeeded with -0 errors. - -It is therefore left as-is: making it well-formed would require either a non-standard wrapper -root or splitting the pack per .NET version, both of which carry more risk than the malformed -file does while nothing reads it. Check C6 parses it with a line-oriented regex rather than an -XML parser, matching how the file is actually produced and consumed. If a future SDK starts -reading it, C6 and this note are the places to revisit. +## `RuntimeList.xml` and self-contained publishing + +`src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml` previously contained one +`` element per supported .NET version, i.e. **multiple root elements**, which +is not well-formed XML. + +**This file is parsed.** `Microsoft.NET.Build.Tasks` contains the literal `RuntimeList.xml` +alongside the runtime-pack manifest fields it reads (`Managed`, `Native`, `PgoData`, +`Resources`, `AssemblyVersion`, `FileVersion`, `PublicKeyToken`), i.e. +`ResolveRuntimePackAssets` reads it whenever runtime pack assets are resolved — notably for +`SelfContained=true`. An earlier note in this file claimed it was never parsed; that claim was +based on a framework-dependent RID build, which does not reach that task. **It was wrong and is +retracted.** + +Both remediations are applied: + +1. **The file is now well-formed** — a single `` root. The pack is a placeholder that + ships no runtime binaries (its only payload is `lib/net6.0-tizen/_._`), so the list is + legitimately empty. +2. **Self-contained Tizen publishing is rejected up front** with `TIZENSDK001`, explaining that + Tizen applications run against the platform-provided runtime. Without it, the build failed + with the opaque `NETSDK1083: The specified RuntimeIdentifier 'tizen' is not recognized`. + +Verified on `11.0.100-preview.7.26381.103`: `dotnet build -p:SelfContained=true` on a +`net11.0-tizen11.0` project now fails with `TIZENSDK001` and the actionable message. +`SkipTizenSelfContainedCheck=true` bypasses the guard for anyone supplying a runtime by other +means. + +**Scope note, stated precisely:** with the guard bypassed *and* the old malformed file restored, +this configuration failed at `NETSDK1083` (RID resolution) *before* reaching +`ResolveRuntimePackAssets`, so a raw `XmlException` could not be reproduced on this SDK. The +malformed file was nevertheless a latent hazard on any path that does reach that task, and both +fixes are correct regardless of which error surfaces first. + +Check C6 now parses the file with a real XML parser and asserts the `TIZENSDK001` guard exists; +`test-matrix.sh` additionally asserts the runtime disposition end to end. diff --git a/workload/scripts/test-install-failure.sh b/workload/scripts/test-install-failure.sh index 0007e0728..b19928b3a 100755 --- a/workload/scripts/test-install-failure.sh +++ b/workload/scripts/test-install-failure.sh @@ -87,6 +87,218 @@ else printf " %sSKIP%s %-52s (pwsh unavailable)\n" "$c_yellow" "$c_reset" "ps1 parity" fi +# --- 4. fallback must resolve the package ID, not just the version ------------- +# +# getLatestVersion previously returned only a version. The caller then downloaded that +# version under the ORIGINAL, unpublished manifest id - e.g. a request for +# '...manifest-10.0.400' resolved to version 10.0.127 (which belongs to +# '...manifest-10.0.300') and then 404'd trying to fetch 10.0.400/10.0.127. +# The function must return "=". + +echo "" +echo "-- fallback resolves package id --" + +# Load the shipped map + function without executing the installer body. +fallback_probe() { + bash -c ' + eval "$(sed -n "/^MANIFEST_BASE_NAME=/p" '"$SH_SCRIPT"')" + eval "$(sed -n "/# BEGIN AUTO-GENERATED VERSION MAP/,/# END AUTO-GENERATED VERSION MAP/p" '"$SH_SCRIPT"' | grep -v "^#")" + eval "$(sed -n "/^function getLatestVersion/,/^}/p" '"$SH_SCRIPT"')" + getLatestVersion "$1" + ' _ "$1" +} + +# "||" ('' = must resolve to nothing) +FALLBACK_CASES=( + "10.0.400|10.0.300|10.0.127" + "10.0.300|10.0.300|10.0.127" + "9.0.400|9.0.300|10.0.121" + "11.0.100-preview.7||" + "12.0.100||" +) + +for case in "${FALLBACK_CASES[@]}"; do + IFS='|' read -r req want_band want_ver <<< "$case" + base="samsung.net.sdk.tizen.manifest" + got="$(fallback_probe "$base-$req")" + if [[ -z "$want_band" ]]; then + if [[ -z "$got" ]]; then + printf " %sPASS%s %-24s -> resolves to nothing (fails closed)\n" "$c_green" "$c_reset" "$req" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-24s -> %s (expected nothing)\n" "$c_red" "$c_reset" "$req" "$got" + fail=$((fail + 1)) + fi + continue + fi + want="$base-$want_band=$want_ver" + if [[ "$got" == "$want" ]]; then + printf " %sPASS%s %-24s -> %s\n" "$c_green" "$c_reset" "$req" "${got#$base-}" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-24s -> %s (expected %s)\n" "$c_red" "$c_reset" "$req" "${got:-}" "$want" + fail=$((fail + 1)) + fi +done + +# --- 5. PowerShell parity, incl. no cross-SDK fallback leakage ----------------- +# +# The PS installer kept the resolved fallback id in a script-level $global:FallbackId that +# was never cleared, so an -UpdateAllWorkloads run could carry one SDK's fallback package +# into the NEXT SDK's install. The resolved id must be per-call. + +if command -v pwsh >/dev/null 2>&1 && [[ -f "$PS1_SCRIPT" ]]; then + echo "" + echo "-- PowerShell fallback parity / no global leakage --" + + if grep -q 'global:FallbackId' "$PS1_SCRIPT"; then + printf " %sFAIL%s workload-install.ps1 still uses \$global:FallbackId\n" "$c_red" "$c_reset" + fail=$((fail + 1)) + else + printf " %sPASS%s workload-install.ps1 has no \$global:FallbackId\n" "$c_green" "$c_reset" + pass=$((pass + 1)) + fi + + cat > "$TMPROOT/ps-probe.ps1" <<'PSEOF' +param([string]$ScriptPath) +$src = Get-Content -Raw $ScriptPath +$ManifestBaseName = 'Samsung.NET.Sdk.Tizen.Manifest' +Invoke-Expression ([regex]::Match($src,'(?s)# BEGIN AUTO-GENERATED VERSION MAP.*?# END AUTO-GENERATED VERSION MAP').Value -replace '(?m)^#.*$','') +Invoke-Expression ([regex]::Match($src,'(?s)# BEGIN VERSION BAND DETECTION.*?# END VERSION BAND DETECTION').Value) +function Resolve-Offline([string]$Id) { + if ($LatestVersionMap.Contains($Id)) { return "$Id=$($LatestVersionMap.$Id)" } + $p = Get-BandFamilyPrefix -ManifestId $Id + if ($p) { + $ids = @(); $vs = @() + foreach ($k in $LatestVersionMap.Keys) { + if ($k -like "$p*") { $ids += $k; $vs += $LatestVersionMap[$k] } + } + if ($vs) { return "$($ids[-1])=$($vs[-1])" } + } + return '' +} +# Mixed-band sequence: a 10.x fallback must not bleed into the 11.x iteration. +foreach ($b in @('10.0.400','11.0.100-preview.7','9.0.400')) { + Write-Output "$b=>$(Resolve-Offline "$ManifestBaseName-$b")" +} +PSEOF + ps_out="$(pwsh -NoProfile -File "$TMPROOT/ps-probe.ps1" -ScriptPath "$PS1_SCRIPT" 2>/dev/null | tr -d '\r')" + + check_ps() { + local label="$1" expect="$2" + if grep -Fqx "$expect" <<< "$ps_out"; then + printf " %sPASS%s %-24s -> %s\n" "$c_green" "$c_reset" "$label" "${expect#*=>}" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-24s (got: %s)\n" "$c_red" "$c_reset" "$label" "$(grep -F "$label=>" <<< "$ps_out")" + fail=$((fail + 1)) + fi + } + B=Samsung.NET.Sdk.Tizen.Manifest + check_ps "10.0.400" "10.0.400=>$B-10.0.300=10.0.127" + check_ps "11.0.100-preview.7" "11.0.100-preview.7=>" + check_ps "9.0.400" "9.0.400=>$B-9.0.300=10.0.121" +else + echo "" + echo " (pwsh unavailable - skipping PowerShell fallback parity)" +fi + +# --- 6. bash 3.2 compatibility ------------------------------------------------- +# +# macOS ships bash 3.2 and is a supported target (DOTNET_DEFAULT_PATH_MACOS). The +# ${var,,} lowercase expansion is bash 4+ and raises "bad substitution" there, which left +# the version empty and silently skipped the fallback path entirely. + +echo "" +echo "-- bash 3.2 compatibility --" + +if grep -nE '\$\{[A-Za-z_][A-Za-z0-9_]*(,,|\^\^)\}' "$SH_SCRIPT" | grep -qv '^\s*[0-9]*:\s*#'; then + printf " %sFAIL%s workload-install.sh uses a bash 4+ case-conversion expansion\n" "$c_red" "$c_reset" + grep -nE '\$\{[A-Za-z_][A-Za-z0-9_]*(,,|\^\^)\}' "$SH_SCRIPT" | sed 's/^/ /' + fail=$((fail + 1)) +else + printf " %sPASS%s no bash 4+ case-conversion expansions\n" "$c_green" "$c_reset" + pass=$((pass + 1)) +fi + +for bad in 'declare -A' 'readarray' 'mapfile'; do + if grep -q -- "$bad" "$SH_SCRIPT"; then + printf " %sFAIL%s workload-install.sh uses bash 4+ feature: %s\n" "$c_red" "$c_reset" "$bad" + fail=$((fail + 1)) + else + printf " %sPASS%s no bash 4+ feature: %-12s\n" "$c_green" "$c_reset" "$bad" + pass=$((pass + 1)) + fi +done + +printf " %sINFO%s running under bash %s\n" "$c_yellow" "$c_reset" "${BASH_VERSION}" + +# --- 7. install path containing spaces ------------------------------------------ +# +# Unquoted $DOTNET_INSTALL_DIR / $TMPDIR expansions word-split on a path with spaces. + +echo "" +echo "-- space-containing install path --" + +SPACEDIR="$TMPROOT/dir with spaces/dotnet sdk" +mkdir -p "$SPACEDIR" +cat > "$SPACEDIR/dotnet" <<'STUB' +#!/bin/bash +case "$1" in + --version) echo "10.0.100" ;; + --list-sdks) echo "10.0.100 [$(dirname "$0")/sdk]" ;; + workload) exit 0 ;; + new) exit 0 ;; + *) exit 0 ;; +esac +STUB +chmod +x "$SPACEDIR/dotnet" + +if curl -sSf -m 20 -o /dev/null https://api.nuget.org/v3/index.json 2>/dev/null; then + space_out="$(cd "$TMPROOT" && bash "$SH_SCRIPT" -d "$SPACEDIR" 2>&1)"; space_rc=$? + if [[ $space_rc -eq 0 ]] && [[ -f "$SPACEDIR/sdk-manifests/10.0.100/samsung.net.sdk.tizen/WorkloadManifest.json" ]]; then + printf " %sPASS%s installs into a path containing spaces\n" "$c_green" "$c_reset" + pass=$((pass + 1)) + else + printf " %sFAIL%s install into space-containing path failed (exit %s)\n" "$c_red" "$c_reset" "$space_rc" + echo "$space_out" | tail -6 | sed 's/^/ | /' + fail=$((fail + 1)) + fi +else + printf " %sSKIP%s space-path install (no network)\n" "$c_yellow" "$c_reset" +fi + +# --- 8. transport failure must fail closed -------------------------------------- +# +# A failed/empty version query must take the fallback path and, when that yields +# nothing, fail - never proceed with an empty version. + +echo "" +echo "-- transport failure fails closed --" + +FAKEHOME="$TMPROOT/nonet" +mkdir -p "$FAKEHOME" +cat > "$FAKEHOME/dotnet" <<'STUB' +#!/bin/bash +case "$1" in + --version) echo "99.0.100" ;; + --list-sdks) echo "99.0.100 [$(dirname "$0")/sdk]" ;; + *) exit 0 ;; +esac +STUB +chmod +x "$FAKEHOME/dotnet" +# Force every curl to fail by pointing at an unroutable proxy. +nonet_out="$(cd "$TMPROOT" && ALL_PROXY="http://127.0.0.1:9" HTTPS_PROXY="http://127.0.0.1:9" \ + bash "$SH_SCRIPT" -d "$FAKEHOME" 2>&1)"; nonet_rc=$? +if [[ $nonet_rc -ne 0 ]] && ! grep -q "^DONE$" <<< "$nonet_out"; then + printf " %sPASS%s unreachable feed -> non-zero exit, no DONE\n" "$c_green" "$c_reset" + pass=$((pass + 1)) +else + printf " %sFAIL%s unreachable feed -> exit %s (must fail closed)\n" "$c_red" "$c_reset" "$nonet_rc" + echo "$nonet_out" | tail -6 | sed 's/^/ | /' + fail=$((fail + 1)) +fi + echo "" echo "============= install-failure summary =============" echo " passed: $pass" diff --git a/workload/scripts/test-matrix.sh b/workload/scripts/test-matrix.sh index 591354590..eab40b9e2 100644 --- a/workload/scripts/test-matrix.sh +++ b/workload/scripts/test-matrix.sh @@ -52,12 +52,20 @@ done # run against the .NET 10 band stays green. To exercise these rows: # make test-matrix DOTNET_VERSION=11.0.100-preview.7.26381.103 MATRIX=( + # net8.0: oldest .NET major still exercised, across every current platform band. "net8.0-tizen10.0|10" "net8.0-tizen10.1|10.1" "net8.0-tizen11.0|11" + # net9.0 "net9.0-tizen10.0|10" - "net11.0-tizen11.0|11" + # net10.0: the current shipping band. Covered explicitly rather than implied by the + # SDK version used to run the matrix. + "net10.0-tizen10.0|10" + "net10.0-tizen10.1|10.1" + "net10.0-tizen11.0|11" + # net11.0: the band this branch adds. "net11.0-tizen10.0|10" + "net11.0-tizen11.0|11" ) # --- helpers --------------------------------------------------------------- @@ -233,6 +241,42 @@ for entry in "${MATRIX[@]}"; do fi done +# --- self-contained disposition -------------------------------------------- +# +# Samsung.NETCore.App.Runtime.tizen is a placeholder pack with no runtime binaries, so a +# self-contained Tizen publish cannot work. It must fail with the actionable TIZENSDK001 +# rather than a raw XmlException from ResolveRuntimePackAssets parsing RuntimeList.xml, +# or an opaque NETSDK1083. +if [[ -z "$ONLY" && $pass_count -gt 0 ]]; then + sc_dir="$TMPDIR/selfcontained" + log "" + log "==> [self-contained disposition]" + rm -rf "$sc_dir" && mkdir -p "$sc_dir" + # Reuse whichever row built successfully; any Tizen project will do. + src_row="$(find "$TMPDIR" -maxdepth 1 -name 'net*-tizen*' -type d | head -1)" + if [[ -n "$src_row" ]]; then + cp "$src_row/TizenApp1.csproj" "$src_row/tizen-manifest.xml" "$sc_dir/" 2>/dev/null + cp -r "$src_row"/*.cs "$sc_dir/" 2>/dev/null + sc_log="$sc_dir/selfcontained.log" + if "$DOTNET" build "$sc_dir" --nologo -p:SelfContained=true > "$sc_log" 2>&1; then + fail "self-contained build unexpectedly SUCCEEDED (no runtime is shipped)" + fail_count+=1 + failed_rows+=("selfcontained:unexpected-success") + elif grep -q "TIZENSDK001" "$sc_log"; then + pass "self-contained rejected with TIZENSDK001" + pass_count+=1 + elif grep -qiE "XmlException|multiple root" "$sc_log"; then + fail "self-contained produced a raw XML parse error - RuntimeList.xml is malformed" + grep -iE "XmlException|multiple root" "$sc_log" | head -2 | sed 's/^/ | /' + fail_count+=1 + failed_rows+=("selfcontained:xmlexception") + else + warn "self-contained failed before reaching the Tizen guard (see $sc_log)" + grep -m2 "error" "$sc_log" | sed 's/^/ | /' + fi + fi +fi + # --- summary --------------------------------------------------------------- log "" diff --git a/workload/scripts/test-package-fallback.sh b/workload/scripts/test-package-fallback.sh new file mode 100755 index 000000000..f48f22e43 --- /dev/null +++ b/workload/scripts/test-package-fallback.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# +# Copyright (c) Samsung Electronics. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# +# Evaluation test for PackageTargetFallback compatibility filtering. +# +# FixupNuGetReferences matches a package's lib// sibling directories against +# PackageTargetFallback by NAME ONLY - it performs no compatibility check of its own. +# An unfiltered cross-product therefore lets a net6.0-tizen8.0 build silently pick up +# net6.0-tizen11.0 (newer platform) or net11.0-tizen8.0 (newer .NET) assets. +# +# This test extracts the real filtering block from the shipped +# Samsung.Tizen.Sdk.NuGet.targets (between the BEGIN/END TIZEN PACKAGE FALLBACK markers) +# and evaluates it, asserting both that compatible entries are present and - the point of +# the exercise - that incompatible ones are ABSENT. +# +# Usage: +# bash workload/scripts/test-package-fallback.sh +# make -C workload test-package-fallback +# + +set -uo pipefail + +WORKLOAD_DIR="$(cd "$(dirname "$0")/.." && pwd)" +TARGETS="$WORKLOAD_DIR/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets" +DOTNET="${DOTNET:-dotnet}" +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +c_reset=$'\033[0m'; c_red=$'\033[31m'; c_green=$'\033[32m'; c_yellow=$'\033[33m' +[[ -t 1 ]] || { c_reset=""; c_red=""; c_green=""; c_yellow=""; } + +if ! command -v "$DOTNET" >/dev/null 2>&1; then + echo " ${c_yellow}SKIP${c_reset} '$DOTNET' not found; cannot evaluate MSBuild expressions." + exit 0 +fi + +BLOCK="$(sed -n '/BEGIN TIZEN PACKAGE FALLBACK/,/END TIZEN PACKAGE FALLBACK/p' "$TARGETS" \ + | sed -e '1d' -e '$d')" +if [[ -z "$BLOCK" ]]; then + echo "ERROR: TIZEN PACKAGE FALLBACK markers not found in $TARGETS." + echo " Keep the markers intact so this test exercises shipped code." + exit 2 +fi + +{ + echo '' + echo "$BLOCK" + echo ' ' + echo ' ' + echo ' ' + echo '' +} > "$TMPDIR/probe.proj" + +pass=0; fail=0 + +# "|||" +CASES=( + # Building for the lowest supported platform: nothing newer may leak in. + "v6.0|8.0|net6.0-tizen8.0,tizen80|net6.0-tizen9.0,net6.0-tizen10.0,net6.0-tizen11.0,net8.0-tizen8.0,net11.0-tizen11.0,tizen90,tizen10.0" + # Newer .NET, old platform: platform siblings above 8.0 must stay out. + "v11.0|8.0|net11.0-tizen8.0,net6.0-tizen8.0,tizen80|net11.0-tizen9.0,net6.0-tizen11.0,net11.0-tizen11.0,tizen90" + # Old .NET, newest platform: .NET majors above 6.0 must stay out. + "v6.0|11.0|net6.0-tizen11.0,net6.0-tizen8.0,tizen10.0|net8.0-tizen11.0,net11.0-tizen11.0,net9.0-tizen10.0" + # The primary target: everything at or below is fair game. + "v11.0|11.0|net11.0-tizen11.0,net6.0-tizen8.0,net8.0-tizen10.0,tizen40|" + # Mid-range combination. + "v9.0|10.0|net9.0-tizen10.0,net8.0-tizen9.0,tizen10.0|net10.0-tizen10.0,net11.0-tizen11.0,net9.0-tizen10.1,net9.0-tizen11.0" + # 10.1 must not admit 11.0, and 10.1 itself is available at 10.1. + "v10.0|10.1|net10.0-tizen10.1,net10.0-tizen10.0|net10.0-tizen11.0,net11.0-tizen10.1" +) + +for case in "${CASES[@]}"; do + IFS='|' read -r tfv tpv want_present want_absent <<< "$case" + + out="$("$DOTNET" msbuild "$TMPDIR/probe.proj" -t:Probe -nologo -v:m \ + -p:TargetFrameworkVersion="$tfv" -p:TargetPlatformVersion="$tpv" 2>&1 \ + | grep -o 'RESULT|.*' | head -1)" + list=";${out#RESULT|};" + list="${list// /}" + + label="net${tfv#v}-tizen${tpv}" + row_ok=1 + detail="" + + presents=(); absents=() + [[ -n "$want_present" ]] && IFS=',' read -ra presents <<< "$want_present" + [[ -n "$want_absent" ]] && IFS=',' read -ra absents <<< "$want_absent" + + for e in "${presents[@]+"${presents[@]}"}"; do + [[ -z "$e" || "$e" == *_SKIP ]] && continue + if [[ "$list" != *";$e;"* ]]; then row_ok=0; detail="$detail missing:$e"; fi + done + + for e in "${absents[@]+"${absents[@]}"}"; do + [[ -z "$e" ]] && continue + if [[ "$list" == *";$e;"* ]]; then row_ok=0; detail="$detail LEAKED:$e"; fi + done + + if [[ $row_ok -eq 1 ]]; then + printf " %sPASS%s %-22s\n" "$c_green" "$c_reset" "$label" + pass=$((pass + 1)) + else + printf " %sFAIL%s %-22s%s\n" "$c_red" "$c_reset" "$label" "$detail" + printf " list: %s\n" "${out#RESULT|}" + fail=$((fail + 1)) + fi +done + +echo "" +echo "=========== package-fallback summary ===========" +echo " passed: $pass" +echo " failed: $fail" + +[[ $fail -eq 0 ]] || exit 1 +exit 0 diff --git a/workload/scripts/validate-workload-metadata.py b/workload/scripts/validate-workload-metadata.py index d33f2701c..11b68f2e3 100644 --- a/workload/scripts/validate-workload-metadata.py +++ b/workload/scripts/validate-workload-metadata.py @@ -30,12 +30,17 @@ matching KnownRuntimePack in Samsung.Tizen.Sdk.targets. Without it the SDK cannot resolve Samsung.NETCore.App.Runtime.tizen and the build fails with NETSDK1082 ("no runtime pack available"). - C6 Every KnownRuntimePack .NET major has a FileList row in RuntimeList.xml. + C6 RuntimeList.xml is well-formed XML with a single root. It is parsed by + ResolveRuntimePackAssets (e.g. for SelfContained=true), so multiple roots throw a + raw XmlException. The pack ships no runtime binaries, so the list is empty and + self-contained publishing is rejected by TIZENSDK001 instead. C7 DotNet11SdkVersion exists in Versions.props and no workflow hardcodes a different .NET 11 SDK version (the workflows must grep the SSOT). - C8 PackageTargetFallback covers every (.NET major from KnownRuntimePack) x - (platform from TizenSdkSupportedTargetPlatformVersion) combination. A missing - entry silently downgrades a package to its netstandard2.x assets. + C8 The PackageTargetFallback candidate list covers every (.NET major from + KnownRuntimePack) x (platform from TizenSdkSupportedTargetPlatformVersion) + combination, and every candidate is emitted with a compatibility Condition. + A missing entry silently downgrades a package to its netstandard2.x assets; + an unconditional entry lets an incompatible TFM's assets be substituted. Run from anywhere — paths derive from this script's location. @@ -46,6 +51,7 @@ from __future__ import annotations import re import sys +import xml.etree.ElementTree as ET from pathlib import Path WORKLOAD_DIR = Path(__file__).resolve().parents[1] @@ -208,18 +214,33 @@ def warn(msg): ") all have a KnownRuntimePack") # --- C6 --- - runtime_list_vers = { - "net" + v for v in re.findall(r'TargetFrameworkVersion="([\d.]+)"', runtime_list) - } - missing_rl = sorted(krp_tfms - runtime_list_vers, key=lambda v: float(v[3:])) if krp_tfms else [] - if not runtime_list_vers: - err("C6: no FileList rows parsed from Samsung.NETCore.App.Runtime/data/RuntimeList.xml") - elif missing_rl: - err("C6: RuntimeList.xml has no FileList row for " + str(missing_rl) + - " but a KnownRuntimePack declares it.") - else: - ok("C6: KnownRuntimePack majors (" + str(len(krp_tfms)) + - ") all present in RuntimeList.xml") + rl_path = WORKLOAD_DIR / "src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml" + c6_ok = True + try: + rl_root = ET.parse(rl_path).getroot() + except ET.ParseError as exc: + err("C6: RuntimeList.xml is not well-formed XML (" + str(exc) + "). " + "ResolveRuntimePackAssets parses this file - e.g. for SelfContained=true - and " + "multiple roots surface as a raw XmlException.") + rl_root = None + c6_ok = False + if rl_root is not None: + if rl_root.tag != "FileList": + err("C6: RuntimeList.xml root is <" + rl_root.tag + ">, expected ") + c6_ok = False + # The pack is a placeholder with no runtime binaries. If files ever appear here, + # the self-contained rejection below needs revisiting. + files = rl_root.findall("File") + sdk_targets_txt = read("src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.targets") + has_guard = "TIZENSDK001" in sdk_targets_txt + if not files and not has_guard: + err("C6: RuntimeList.xml lists no runtime files, but Samsung.Tizen.Sdk.targets has " + "no TIZENSDK001 self-contained guard. A self-contained publish would emit an " + "app with no runtime.") + c6_ok = False + if c6_ok: + ok("C6: RuntimeList.xml well-formed (single root, " + str(len(files)) + + " file(s)); self-contained guarded by TIZENSDK001") # --- C7 --- net11_sdk = props.get("DotNet11SdkVersion", "") @@ -230,9 +251,17 @@ def warn(msg): workflow_dir = WORKLOAD_DIR.parent / ".github" / "workflows" stray = [] for wf in sorted(workflow_dir.glob("*.yml")): - text = wf.read_text(encoding="utf-8") - for literal in set(re.findall(r"\b11\.0\.\d{3}-[A-Za-z0-9.]+", text)): - if literal != net11_sdk: + # Strip comments: prose may legitimately mention a band or an example + # version. Only real values can cause CI to use the wrong SDK. + lines = [] + for line in wf.read_text(encoding="utf-8").splitlines(): + stripped = line.lstrip() + if stripped.startswith("#"): + continue + lines.append(line.split(" #", 1)[0]) + text = "\n".join(lines) + for literal in set(re.findall(r"\b11\.0\.\d{3}-[A-Za-z0-9]+(?:\.[A-Za-z0-9]+)*", text)): + if literal.rstrip(".") != net11_sdk: stray.append(wf.name + ": " + literal) if stray: err("C7: workflow(s) hardcode a .NET 11 SDK version differing from " @@ -242,25 +271,36 @@ def warn(msg): # --- C8 --- nuget_targets = read("src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets") - ptf_match = re.search(r"(.*?)", - nuget_targets, re.S) - if not ptf_match: - err("C8: PackageTargetFallback not found in Samsung.Tizen.Sdk.NuGet.targets") + # Each candidate is a conditional _TizenFallbackList append. Capture the appended TFM + # and whether the line carries a compatibility Condition. + cand_lines = re.findall( + r'<_TizenFallbackList(\s+Condition="[^"]*")?>\$\(_TizenFallbackList\);([^<]+)', + nuget_targets) + if not cand_lines: + err("C8: no _TizenFallbackList candidates found in Samsung.Tizen.Sdk.NuGet.targets") elif not krp_tfms or not supported_set: err("C8: cannot evaluate - KnownRuntimePack or supported platform list empty") else: - listed = set( - e.strip() for e in ptf_match.group(1).replace("\n", "").split(";") if e.strip() - ) + listed = {tfm.strip() for _cond, tfm in cand_lines} + unconditional = sorted(tfm.strip() for cond, tfm in cand_lines if not cond) expected = {m + "-tizen" + p for m in krp_tfms for p in supported_set} missing_ptf = sorted(expected - listed) + c8_ok = True if missing_ptf: err("C8: PackageTargetFallback missing " + str(missing_ptf) + ". A package shipping lib// alongside netstandard2.x would silently " "resolve to the netstandard assets for those TFMs.") - else: + c8_ok = False + if unconditional: + err("C8: PackageTargetFallback candidate(s) " + str(unconditional) + + " have no compatibility Condition. FixupNuGetReferences matches by name " + "only, so an unconditional entry lets an incompatible TFM's assets be " + "substituted (e.g. net6.0-tizen11.0 into a net6.0-tizen8.0 build).") + c8_ok = False + if c8_ok: ok("C8: PackageTargetFallback covers all " + str(len(expected)) + - " supported (.NET major x platform) combinations") + " supported (.NET major x platform) combinations; all " + + str(len(cand_lines)) + " candidates are compatibility-gated") print() if errors: diff --git a/workload/scripts/workload-install.ps1 b/workload/scripts/workload-install.ps1 index 90436bbdb..8f0b5fada 100644 --- a/workload/scripts/workload-install.ps1 +++ b/workload/scripts/workload-install.ps1 @@ -27,7 +27,6 @@ $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $ManifestBaseName = "Samsung.NET.Sdk.Tizen.Manifest" -$global:FallbackId = "" # BEGIN AUTO-GENERATED VERSION MAP -- edit version-map.json and rerun Generate-InstallScripts.ps1 $LatestVersionMap = [ordered]@{ @@ -96,7 +95,7 @@ function Get-LatestVersion([string]$Id) { try { $Response = Invoke-WebRequest -Uri https://api.nuget.org/v3-flatcontainer/$($Id.ToLowerInvariant())/index.json -UseBasicParsing | ConvertFrom-Json - return $Response.versions | Select-Object -Last 1 + return "$Id=$($Response.versions | Select-Object -Last 1)" } catch { Write-Host "Id: $Id" @@ -110,7 +109,7 @@ function Get-LatestVersion([string]$Id) { if ($LatestVersionMap.Contains($Id)) { Write-Host "Return cached latest version." - return $LatestVersionMap.$Id + return "$Id=$($LatestVersionMap.$Id)" } else { @@ -132,10 +131,10 @@ function Get-LatestVersion([string]$Id) { } if ($MatchingFallbackVersion) { - $global:FallbackId = $MatchingFallbackId[-1] + $FallbackId = $MatchingFallbackId[-1] $FallbackVersion = $MatchingFallbackVersion[-1] - Write-Host "Return fallback version: $FallbackVersion (from $($MatchingFallbackId[-1]))" - return $FallbackVersion + Write-Host "Return fallback version: $FallbackVersion (from $FallbackId)" + return "$FallbackId=$FallbackVersion" } } } @@ -249,8 +248,19 @@ function Install-TizenWorkload([string]$DotnetVersion) } # Check latest version of manifest. + # + # Get-LatestVersion returns "=". The id matters: when the + # requested band has no published manifest we fall back to an earlier band's + # package, and that version does not exist under the requested id. Both values are + # kept function-local so an -UpdateAllWorkloads run cannot carry one SDK's fallback + # package into the next SDK's install. if ($Version -eq "" -or $UpdateAllWorkloads.IsPresent) { - $Version = Get-LatestVersion -Id $ManifestName + $Resolved = Get-LatestVersion -Id $ManifestName + if (-not $Resolved) { + throw "Failed to resolve a manifest package for $ManifestName." + } + $ManifestName = $Resolved.Substring(0, $Resolved.LastIndexOf("=")) + $Version = $Resolved.Substring($Resolved.LastIndexOf("=") + 1) } # Check workload manifest directory. @@ -285,11 +295,7 @@ function Install-TizenWorkload([string]$DotnetVersion) # Install workload manifest. Write-Host "Installing $ManifestName/$Version to $ManifestDir..." - if ($global:FallbackId) { - Install-Pack -Id $global:FallbackId -Version $Version -Kind "manifest" - } else { - Install-Pack -Id $ManifestName -Version $Version -Kind "manifest" - } + Install-Pack -Id $ManifestName -Version $Version -Kind "manifest" # Download and install workload packs. $NewManifestJson = $(Get-Content $TizenManifestFile | ConvertFrom-Json) diff --git a/workload/scripts/workload-install.sh b/workload/scripts/workload-install.sh index dafc1eee1..d6c175302 100755 --- a/workload/scripts/workload-install.sh +++ b/workload/scripts/workload-install.sh @@ -63,7 +63,7 @@ while [ $# -ne 0 ]; do ;; -d|--dotnet-install-dir) shift - DOTNET_INSTALL_DIR=$1 + DOTNET_INSTALL_DIR="$1" ;; -t|--dotnet-target-version-band) shift @@ -95,7 +95,7 @@ while [ $# -ne 0 ]; do done function read_dotnet_link() { - cd -P "$(dirname "$1")" + cd -P "$(dirname "$1")" || return 1 dotnet_file="$PWD/$(basename "$1")" while [[ -h "$dotnet_file" ]]; do cd -P "$(dirname "$dotnet_file")" @@ -112,33 +112,41 @@ function error_permission_denied() { } function ensure_directory() { - if [ ! -d $1 ]; then - mkdir -p $1 || error_permission_denied + if [ ! -d "$1" ]; then + mkdir -p "$1" || error_permission_denied fi - [ ! -w $1 ] && error_permission_denied + [ ! -w "$1" ] && error_permission_denied } # Check dotnet install directory. if [[ "$DOTNET_INSTALL_DIR" == "" ]]; then if [[ -n "$DOTNET_ROOT" && -d "$DOTNET_ROOT" ]]; then - DOTNET_INSTALL_DIR=$DOTNET_ROOT + DOTNET_INSTALL_DIR="$DOTNET_ROOT" elif [[ -d "$DOTNET_DEFAULT_PATH_LINUX" ]]; then - DOTNET_INSTALL_DIR=$DOTNET_DEFAULT_PATH_LINUX + DOTNET_INSTALL_DIR="$DOTNET_DEFAULT_PATH_LINUX" elif [[ -d "$DOTNET_DEFAULT_PATH_MACOS" ]]; then - DOTNET_INSTALL_DIR=$DOTNET_DEFAULT_PATH_MACOS + DOTNET_INSTALL_DIR="$DOTNET_DEFAULT_PATH_MACOS" elif [[ -n "$(which dotnet)" ]]; then - DOTNET_INSTALL_DIR=$(read_dotnet_link $(which dotnet)) + DOTNET_INSTALL_DIR="$(read_dotnet_link "$(which dotnet)")" fi fi -if [ ! -d $DOTNET_INSTALL_DIR ]; then +if [ ! -d "$DOTNET_INSTALL_DIR" ]; then echo "No installed dotnet \`$DOTNET_INSTALL_DIR\`." exit 1 fi +# Resolve the manifest package to install for a requested manifest id. +# +# Echoes "=", or the empty string when nothing is available. +# The package id MUST be returned alongside the version: when the requested band has +# no published manifest we fall back to an EARLIER band's package, and downloading +# that version under the originally requested (non-existent) id 404s. For example +# a request for '...manifest-10.0.400' resolves to '...manifest-10.0.300=10.0.127' +# and the 10.0.300 package is what has to be downloaded. function getLatestVersion () { for index in "${LatestVersionMap[@]}"; do if [ "${index%%=*}" = "${1}" ]; then - echo "${index#*=}" + echo "${1}=${index#*=}" return fi done @@ -153,15 +161,21 @@ function getLatestVersion () { return fi local prefix="${manifestId%%-*}-${family}." + local fallbackId="" local fallbackVersion="" for entry in "${LatestVersionMap[@]}"; do mapKey="${entry%%=*}" mapValue="${entry#*=}" if [[ "$mapKey" == "$prefix"* ]]; then + fallbackId="$mapKey" fallbackVersion="$mapValue" fi done - echo "$fallbackVersion" + if [[ -z "$fallbackId" ]]; then + echo "" + return + fi + echo "$fallbackId=$fallbackVersion" } # Check installed dotnet version @@ -220,11 +234,22 @@ function install_tizenworkload() { # Check latest version of manifest. if [[ "$MANIFEST_VERSION" == "" ]]; then - MANIFEST_VERSION=$(curl -s https://api.nuget.org/v3-flatcontainer/${MANIFEST_NAME,,}/index.json | grep \" | tail -n 1 | tr -d '\r' | xargs) - if [ -n "$MANIFEST_VERSION" ] && echo "$MANIFEST_VERSION" | grep -q "BlobNotFound"; then - MANIFEST_VERSION=$(getLatestVersion "$MANIFEST_NAME") - if [[ -n $MANIFEST_VERSION ]]; then - echo "Return cached latest version: $MANIFEST_VERSION" + # NOTE: use tr, not the ${var,,} expansion. That expansion is bash 4+, and macOS + # still ships bash 3.2 - where it raises "bad substitution", leaves + # MANIFEST_VERSION empty and silently skips the fallback below. macOS is a + # supported target (see DOTNET_DEFAULT_PATH_MACOS). + MANIFEST_NAME_LOWER=$(echo "$MANIFEST_NAME" | tr '[:upper:]' '[:lower:]') + MANIFEST_VERSION=$(curl -s https://api.nuget.org/v3-flatcontainer/$MANIFEST_NAME_LOWER/index.json | grep \" | tail -n 1 | tr -d '\r' | xargs) + # An empty response (network failure, or the package having never been published) + # must take the same fallback path as an explicit BlobNotFound. + if [ -z "$MANIFEST_VERSION" ] || echo "$MANIFEST_VERSION" | grep -q "BlobNotFound"; then + RESOLVED_MANIFEST=$(getLatestVersion "$MANIFEST_NAME") + if [[ -n $RESOLVED_MANIFEST ]]; then + # Download the package that actually exists. The manifest is still + # installed into the requested band's directory below. + MANIFEST_NAME="${RESOLVED_MANIFEST%%=*}" + MANIFEST_VERSION="${RESOLVED_MANIFEST#*=}" + echo "Return cached latest version: $MANIFEST_NAME/$MANIFEST_VERSION" else echo "Failed to get the latest version of $MANIFEST_NAME." return 1 @@ -233,29 +258,35 @@ function install_tizenworkload() { fi # Check workload manifest directory. - SDK_MANIFESTS_DIR=$DOTNET_INSTALL_DIR/sdk-manifests/$DOTNET_TARGET_VERSION_BAND - ensure_directory $SDK_MANIFESTS_DIR + SDK_MANIFESTS_DIR="$DOTNET_INSTALL_DIR/sdk-manifests/$DOTNET_TARGET_VERSION_BAND" + ensure_directory "$SDK_MANIFESTS_DIR" TMPDIR=$(mktemp -d) echo "Installing $MANIFEST_NAME/$MANIFEST_VERSION to $SDK_MANIFESTS_DIR..." # Download and extract the manifest nuget package. - curl -s -o $TMPDIR/manifest.zip -L https://www.nuget.org/api/v2/package/$MANIFEST_NAME/$MANIFEST_VERSION + curl -sfL -o "$TMPDIR/manifest.zip" "https://www.nuget.org/api/v2/package/$MANIFEST_NAME/$MANIFEST_VERSION" + CURL_STATUS=$? + if [ $CURL_STATUS -ne 0 ]; then + echo "Failed to download $MANIFEST_NAME/$MANIFEST_VERSION (curl exit $CURL_STATUS)." + rm -fr "$TMPDIR" + return 1 + fi - unzip -qq -d $TMPDIR/unzipped $TMPDIR/manifest.zip - if [ ! -d $TMPDIR/unzipped/data ]; then + unzip -qq -d "$TMPDIR/unzipped" "$TMPDIR/manifest.zip" + if [ ! -d "$TMPDIR/unzipped/data" ]; then echo "No such files to install." - rm -fr $TMPDIR + rm -fr "$TMPDIR" return 1 fi - chmod 744 $TMPDIR/unzipped/data/* + chmod 744 "$TMPDIR"/unzipped/data/* # Copy manifest files to dotnet sdk. - mkdir -p $SDK_MANIFESTS_DIR/samsung.net.sdk.tizen - cp -f $TMPDIR/unzipped/data/* $SDK_MANIFESTS_DIR/samsung.net.sdk.tizen/ + mkdir -p "$SDK_MANIFESTS_DIR/samsung.net.sdk.tizen" + cp -f "$TMPDIR"/unzipped/data/* "$SDK_MANIFESTS_DIR/samsung.net.sdk.tizen/" - if [ ! -f $SDK_MANIFESTS_DIR/samsung.net.sdk.tizen/WorkloadManifest.json ]; then + if [ ! -f "$SDK_MANIFESTS_DIR/samsung.net.sdk.tizen/WorkloadManifest.json" ]; then echo "Installation is failed." rm -fr $TMPDIR return 1 @@ -269,11 +300,11 @@ function install_tizenworkload() { CACHE_GLOBAL_JSON="false" fi dotnet new globaljson --sdk-version $DOTNET_VERSION - $DOTNET_INSTALL_DIR/dotnet workload install tizen --skip-manifest-update + "$DOTNET_INSTALL_DIR/dotnet" workload install tizen --skip-manifest-update local install_status=$? # Clean-up - rm -fr $TMPDIR + rm -fr "$TMPDIR" rm global.json if [[ "$CACHE_GLOBAL_JSON" == "true" ]]; then mv global.json.bak global.json @@ -289,9 +320,9 @@ function install_tizenworkload() { } if [[ "$UPDATE_ALL_WORKLOADS" == "true" ]]; then - INSTALLED_DOTNET_SDKS=$($DOTNET_COMMAND --list-sdks | sed -E -n '/^([6-9]|[1-9][0-9]+)\./p' | sed 's/ \[.*//g') + INSTALLED_DOTNET_SDKS=$("$DOTNET_COMMAND" --list-sdks | sed -E -n '/^([6-9]|[1-9][0-9]+)\./p' | sed 's/ \[.*//g') else - INSTALLED_DOTNET_SDKS=$($DOTNET_COMMAND --version) + INSTALLED_DOTNET_SDKS=$("$DOTNET_COMMAND" --version) fi FAILED_SDKS="" diff --git a/workload/src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml b/workload/src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml index c188d2592..903f5d840 100644 --- a/workload/src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml +++ b/workload/src/Samsung.NETCore.App.Runtime/data/RuntimeList.xml @@ -1,6 +1,17 @@ - - - - - - + + + diff --git a/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets b/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets index 54594a578..28ac2b019 100644 --- a/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets +++ b/workload/src/Samsung.Tizen.Sdk/targets/Samsung.Tizen.Sdk.NuGet.targets @@ -14,57 +14,90 @@ Copyright (c) Samsung All rights reserved. + + - - - net11.0-tizen11.0; - net11.0-tizen10.1; - net11.0-tizen10.0; - net11.0-tizen9.0; - net11.0-tizen8.0; - net10.0-tizen11.0; - net10.0-tizen10.1; - net10.0-tizen10.0; - net10.0-tizen9.0; - net10.0-tizen8.0; - net9.0-tizen11.0; - net9.0-tizen10.1; - net9.0-tizen10.0; - net9.0-tizen9.0; - net9.0-tizen8.0; - net8.0-tizen11.0; - net8.0-tizen10.1; - net8.0-tizen10.0; - net8.0-tizen9.0; - net8.0-tizen8.0; - net7.0-tizen11.0; - net7.0-tizen10.1; - net7.0-tizen10.0; - net7.0-tizen9.0; - net7.0-tizen8.0; - net6.0-tizen11.0; - net6.0-tizen10.1; - net6.0-tizen10.0; - net6.0-tizen9.0; - net6.0-tizen8.0; - tizen10.0; - tizen90; - tizen80; - tizen70; - tizen60; - tizen50; - tizen40; - $(PackageTargetFallback); - + + + <_TizenFallbackNetVersion>$(TargetFrameworkVersion.TrimStart('vV')) + <_TizenFallbackNetVersion Condition="'$(_TizenFallbackNetVersion)' == ''">0.0 + <_TizenFallbackPlatformVersion>$(TargetPlatformVersion) + <_TizenFallbackPlatformVersion Condition="'$(_TizenFallbackPlatformVersion)' == ''">$(_DefaultTargetPlatformVersion) + <_TizenFallbackPlatformVersion Condition="'$(_TizenFallbackPlatformVersion)' == ''">0.0 + + + + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net11.0-tizen11.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.1', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net11.0-tizen10.1 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net11.0-tizen10.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net11.0-tizen9.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net11.0-tizen8.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net10.0-tizen11.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.1', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net10.0-tizen10.1 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net10.0-tizen10.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net10.0-tizen9.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net10.0-tizen8.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net9.0-tizen11.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.1', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net9.0-tizen10.1 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net9.0-tizen10.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net9.0-tizen9.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net9.0-tizen8.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net8.0-tizen11.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.1', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net8.0-tizen10.1 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net8.0-tizen10.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net8.0-tizen9.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net8.0-tizen8.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('7.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net7.0-tizen11.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('7.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.1', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net7.0-tizen10.1 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('7.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net7.0-tizen10.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('7.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net7.0-tizen9.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('7.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net7.0-tizen8.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('6.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('11.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net6.0-tizen11.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('6.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.1', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net6.0-tizen10.1 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('6.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net6.0-tizen10.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('6.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net6.0-tizen9.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('6.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);net6.0-tizen8.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('0.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('10.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);tizen10.0 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('0.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('9.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);tizen90 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('0.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('8.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);tizen80 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('0.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('7.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);tizen70 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('0.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('6.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);tizen60 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('0.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('5.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);tizen50 + <_TizenFallbackList Condition="$([MSBuild]::VersionLessThanOrEquals('0.0', '$(_TizenFallbackNetVersion)')) And $([MSBuild]::VersionLessThanOrEquals('4.0', '$(_TizenFallbackPlatformVersion)'))">$(_TizenFallbackList);tizen40 + + + + $(_TizenFallbackList.Trim(';'));$(PackageTargetFallback); + + + + + + From c26d236b85d66eae0186c342770ad05e0087210a Mon Sep 17 00:00:00 2001 From: redth Date: Thu, 27 Aug 2026 09:32:01 -0400 Subject: [PATCH 5/7] fix: address final exact-head review blockers 1. PowerShell empty versions An empty or all-blank versions[] made `Select-Object -Last 1` yield $null, so Get-LatestVersion returned "=" - a TRUTHY string. The caller then split off an empty version, and the NuGet v2 package endpoint serves the LATEST package when the URL has no version segment, silently installing an arbitrary version. Blank entries are now filtered, an empty result falls through to the retry/version-map path, and the caller rejects an empty id or version outright. The shell installer gained the same guard before the download. 2. Shell SDK pinning install_tizenworkload is invoked under `if !`, which disables errexit for everything it calls, so the unchecked `dotnet new globaljson` let the install proceed against whatever SDK PATH resolved. The pin now uses the dotnet under test, its exit status is checked, and the EFFECTIVE `dotnet --version` and feature band are re-verified against the requested ones before any pack is installed. Verified with a stub whose pin silently does not take effect: the run aborts non-zero and never reaches the install. 3. Release versioning / resume The bump was committed and pushed BEFORE the build, so any later failure left the branch already bumped and the retry computed OLD == NEW and aborted - an unretryable release. Persistence now happens only after the build succeeds and the expected artifacts are verified on disk. OLD == NEW resumes instead of aborting, since next-workload-version.py derives from what is published on NuGet and the branch may legitimately already carry the intended unpublished version. Reference-only runs are fully non-mutating (no bump, no commit, no tag), and re-creating an existing tag is a no-op. 4. Fallback priority and atomicity PackageTargetFallback is an ordered preference list, but FixupNuGetReferences collected every matching directory into an unordered HashSet populated in filesystem-enumeration order and then took assemblies first-wins across all of them. That could ignore the declared priority and mix assemblies from different TFMs within one package. Candidates are now ranked by their position in the list, exactly ONE fallback TFM is selected per package, and every substituted assembly comes from that single directory. 5. Self-contained matrix outcome An unexpected diagnostic warned and passed. Self-contained has exactly one supported outcome, so anything other than TIZENSDK001 - including NETSDK1083 or an accidental success - now fails CI, as does being unable to prepare the fixture. Tests: new scripts/test-release-workflow.sh (14 assertions covering ordering, retryability and non-mutating reference-only runs); test-package-fallback.sh gains priority/atomicity cases built in BOTH directory-creation orders so the assertion does not depend on filesystem enumeration; test-install-failure.sh gains SDK-pin verification (effective and ineffective) and empty-response guards for both installers. Validation: make check green - C1-C8 plus 135 assertions (11 self-test, 61 version-band, 13 template-condition, 12 package-fallback, 14 release-workflow, 24 install-failure), all under bash 3.2, including installs into a space-containing path, an unreachable feed, and a pin that does not take effect. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-matrix.yml | 3 + .github/workflows/release-workload.yml | 84 +++++++++---- workload/Makefile | 6 +- workload/docs/net11.md | 47 +++++++- workload/scripts/test-install-failure.sh | 112 +++++++++++++++++ workload/scripts/test-matrix.sh | 15 ++- workload/scripts/test-package-fallback.sh | 104 ++++++++++++++++ workload/scripts/test-release-workflow.sh | 113 ++++++++++++++++++ workload/scripts/workload-install.ps1 | 25 +++- workload/scripts/workload-install.sh | 36 +++++- .../FixupNuGetReference.cs | 88 ++++++++++---- 11 files changed, 575 insertions(+), 58 deletions(-) create mode 100755 workload/scripts/test-release-workflow.sh diff --git a/.github/workflows/build-matrix.yml b/.github/workflows/build-matrix.yml index f5fb01a9f..64247fcc5 100644 --- a/.github/workflows/build-matrix.yml +++ b/.github/workflows/build-matrix.yml @@ -78,6 +78,9 @@ jobs: - name: Run test-package-fallback.sh run: bash workload/scripts/test-package-fallback.sh + - name: Run test-release-workflow.sh + run: bash workload/scripts/test-release-workflow.sh + - name: Run test-install-failure.sh run: bash workload/scripts/test-install-failure.sh diff --git a/.github/workflows/release-workload.yml b/.github/workflows/release-workload.yml index 168a9beb9..1dbab66fb 100644 --- a/.github/workflows/release-workload.yml +++ b/.github/workflows/release-workload.yml @@ -37,43 +37,46 @@ jobs: --store-password-in-clear-text \ --configfile workload/NuGet.config - - name: Bump TizenWorkloadVersion (global sequential, NuGet-derived) + # Resolve the version to release. + # + # Two properties matter here: + # * A reference-only run (release_manifest=false) publishes no manifest, so it must be + # completely NON-MUTATING - no bump, no commit, no tag. + # * OLD == NEW is NOT necessarily an error. next-workload-version.py derives the next + # version from what is published on NuGet, so if a previous run already bumped and + # pushed but failed before publishing, the branch legitimately carries the intended + # (still unpublished) version. That case must RESUME, not abort - otherwise a failed + # release can never be retried. + - name: Resolve release version id: bump working-directory: ./workload run: | - set -e + set -euo pipefail OLD=$(grep -oP '(?<=)[^<]+(?=)' build/Versions.props) NEW=$(python3 scripts/next-workload-version.py) echo "Current: $OLD" echo "Next: $NEW" + + if [ "${{ github.event.inputs.release_manifest }}" != "true" ]; then + echo "::notice ::Reference-only run: leaving TizenWorkloadVersion at $OLD (non-mutating)." + echo "workload_version=$OLD" >> "$GITHUB_OUTPUT" + echo "mutated=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "$OLD" = "$NEW" ]; then - echo "ERROR: computed next == current. NuGet already has this version published." - exit 1 + # The branch already holds the version NuGet does not have yet: resume it. + echo "::notice ::Resuming release of $OLD (already set on this branch, not yet published)." + echo "workload_version=$OLD" >> "$GITHUB_OUTPUT" + echo "mutated=false" >> "$GITHUB_OUTPUT" + exit 0 fi + python3 scripts/next-workload-version.py --apply --verbose echo "workload_version=$NEW" >> "$GITHUB_OUTPUT" + echo "mutated=true" >> "$GITHUB_OUTPUT" echo "::notice ::Bumped TizenWorkloadVersion: $OLD -> $NEW" - # Persist the bump. Without this the tag created below points at a commit whose - # Versions.props still holds the PREVIOUS version, so the released sources do not - # correspond to the published package versions. - - name: Commit and push the version bump - run: | - set -e - BRANCH="${GITHUB_REF_NAME}" - VER="${{ steps.bump.outputs.workload_version }}" - if git diff --quiet -- workload/build/Versions.props; then - echo "::error::Versions.props was not modified by the bump step." - exit 1 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add workload/build/Versions.props - git commit -m "chore: bump TizenWorkloadVersion to ${VER}" - git push origin "HEAD:${BRANCH}" - echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - id: commit - - name: Install Wix toolset run: sudo apt-get install -y wixl @@ -124,6 +127,34 @@ jobs: echo "manifest_pkg=$MANIFEST_PKG" >> "$GITHUB_OUTPUT" echo "Staged packages:"; ls -1 "$STAGING" + # Persist the bump only AFTER the build succeeded and the expected artifacts were + # verified on disk. Committing earlier meant a later build failure left the branch + # already bumped, so the retry computed OLD == NEW and aborted - an unretryable release. + # + # Skipped when the resolve step did not mutate anything (reference-only run, or a resume + # of a version this branch already carries). + - name: Commit and push the version bump + id: commit + run: | + set -euo pipefail + BRANCH="${GITHUB_REF_NAME}" + VER="${{ steps.bump.outputs.workload_version }}" + if [ "${{ steps.bump.outputs.mutated }}" != "true" ]; then + echo "::notice ::No version mutation to persist; tagging existing HEAD." + echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + exit 0 + fi + if git diff --quiet -- workload/build/Versions.props; then + echo "::error::Versions.props was not modified by the resolve step." + exit 1 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add workload/build/Versions.props + git commit -m "chore: bump TizenWorkloadVersion to ${VER}" + git push origin "HEAD:${BRANCH}" + echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Push Manifest/SDK/Runtime packs if: ${{ github.event.inputs.release_manifest == 'true' }} run: | @@ -159,6 +190,7 @@ jobs: -t 3000 \ --skip-duplicate + # Idempotent: a resumed run whose tag already exists must not fail the whole release. - name: Create GitHub Release if: ${{ github.event.inputs.release_manifest == 'true' }} env: @@ -187,6 +219,10 @@ jobs: [ -n "$v" ] && echo "- https://www.nuget.org/packages/Samsung.Tizen.Ref.API${api}/${v}" done } > "$RUNNER_TEMP/release-notes.md" + if gh release view "v${VER}" >/dev/null 2>&1; then + echo "::notice ::Release v${VER} already exists; skipping creation (resumed run)." + exit 0 + fi gh release create "v${VER}" \ --target "${{ steps.commit.outputs.release_sha }}" \ --title "Tizen Workload ${VER} - .Net ${MAJOR_MINOR}" \ diff --git a/workload/Makefile b/workload/Makefile index baf8d6341..baf8cf7d4 100644 --- a/workload/Makefile +++ b/workload/Makefile @@ -163,12 +163,16 @@ test-template-conditions: test-package-fallback: @bash $(TOP)/scripts/test-package-fallback.sh +.PHONY: test-release-workflow +test-release-workflow: + @bash $(TOP)/scripts/test-release-workflow.sh + .PHONY: test-install-failure test-install-failure: @bash $(TOP)/scripts/test-install-failure.sh .PHONY: check -check: validate-metadata test-matrix-self-test test-version-band test-template-conditions test-package-fallback test-install-failure +check: validate-metadata test-matrix-self-test test-version-band test-template-conditions test-package-fallback test-release-workflow test-install-failure @if command -v pwsh >/dev/null 2>&1; then \ pwsh $(TOP)/scripts/Generate-InstallScripts.ps1 -Check; \ else \ diff --git a/workload/docs/net11.md b/workload/docs/net11.md index 8af766e89..468a60a8e 100644 --- a/workload/docs/net11.md +++ b/workload/docs/net11.md @@ -74,8 +74,9 @@ eventual `11.0.100` GA need no further code change. This is asserted by | `test-matrix-self-test` | matrix row selection (an over-strict check silently builds nothing) | | `test-version-band` | SDK version → feature band across **both installers and Config.mk**, fallback band family, band isolation | | `test-template-conditions` | template platform detection across TFMs | -| `test-package-fallback` | `PackageTargetFallback` compatibility filtering, incl. negative cross-platform/version cases | -| `test-install-failure` | installers exit non-zero on failure; fallback resolves package **id**, no cross-SDK leakage | +| `test-package-fallback` | fallback filtering, plus selection priority / atomicity in both directory-creation orders | +| `test-release-workflow` | release ordering, retryability, non-mutating reference-only runs | +| `test-install-failure` | installer exit codes, fallback package **id**, SDK-pin verification, empty/transport responses | | `Generate-InstallScripts.ps1 -Check` | version-map drift **and** install-script integrity | `make check` requires `pwsh` for the last row. If it is unavailable the target fails with an @@ -121,6 +122,18 @@ Two properties are load-bearing: An 11.x request with no 11.x map entry fails closed. +The resolved version is validated before use. An empty or all-blank `versions[]` from the feed +is rejected rather than returned as a truthy `"="`: the NuGet v2 package endpoint serves the +**latest** version when given a versionless URL, so an empty version would silently install an +arbitrary package. Both installers fall through to the version map and then fail closed. + +The SDK pin is verified before anything is installed. `install_tizenworkload` is invoked under +`if !`, which disables `errexit` for everything it calls, so an unchecked +`dotnet new globaljson` previously let the install proceed against whatever SDK `PATH` resolved. +The pin now uses the dotnet under test, its exit status is checked, and the **effective** +`dotnet --version` and feature band are re-verified against the requested ones before any pack +is installed. + Note the installer must run under **bash 3.2** (macOS ships it and is a supported target, see `DOTNET_DEFAULT_PATH_MACOS`). The `${var,,}` lowercase expansion is bash 4+ and raised `bad substitution` there, leaving the version empty and silently skipping the fallback @@ -143,6 +156,15 @@ The filter is built from conditional **properties**, not filtered items: MSBuild top-level properties before any items, so a property referencing `@(item)` at that level silently expands to nothing. +`PackageTargetFallback` is an **ordered preference list**, and `FixupNuGetReferences` honours +that order: it ranks candidates by their position in the list and selects exactly **one** +fallback TFM per package, taking every substituted assembly from that single directory. It +previously collected all matching directories into an unordered `HashSet` populated in +filesystem-enumeration order and then took assemblies first-wins across them, which could both +ignore the declared priority and mix assemblies from different TFMs within one package. +`scripts/test-package-fallback.sh` builds the candidate directories in **both** creation orders +so the assertion does not depend on how a particular filesystem enumerates. + Check C8 verifies the candidate list covers the full (.NET major × platform) cross-product *and* that every candidate carries a compatibility condition; `scripts/test-package-fallback.sh` pins the filtering itself with negative @@ -277,3 +299,24 @@ fixes are correct regardless of which error surfaces first. Check C6 now parses the file with a real XML parser and asserts the `TIZENSDK001` guard exists; `test-matrix.sh` additionally asserts the runtime disposition end to end. + +## Release retryability + +The release workflow is ordered so a failure is always retryable: + +1. resolve the version (no mutation yet), +2. clean, build, and **verify** the expected artifacts on disk, +3. only then commit and push the version bump, +4. push packages from the verified staging directory, +5. create the tag, targeting the commit that carries the released `Versions.props`. + +Committing the bump *before* the build meant a later failure left the branch already bumped, so +the retry computed `OLD == NEW` and aborted — an unretryable release. `OLD == NEW` is therefore +no longer an error: `next-workload-version.py` derives the next version from what is published +on NuGet, so when the branch already carries the intended still-unpublished version the run +**resumes** it. Re-creating an existing tag is a no-op. + +A reference-only run (`release_manifest=false`) publishes no manifest and is completely +non-mutating: no bump, no commit, no tag. + +`scripts/test-release-workflow.sh` pins all of the above. diff --git a/workload/scripts/test-install-failure.sh b/workload/scripts/test-install-failure.sh index b19928b3a..d122fb198 100755 --- a/workload/scripts/test-install-failure.sh +++ b/workload/scripts/test-install-failure.sh @@ -299,6 +299,118 @@ else fail=$((fail + 1)) fi +# --- 9. SDK pin must be verified before installing ----------------------------- +# +# install_tizenworkload is invoked under `if !`, which disables errexit for everything it +# calls. An unchecked `dotnet new globaljson` therefore let the install proceed against +# whatever SDK the PATH happened to resolve. The pin is now checked, and the EFFECTIVE +# version/band re-verified, before any pack is installed. + +echo "" +echo "-- SDK pin verified before install --" + +PINDIR="$TMPROOT/pinbad" +mkdir -p "$PINDIR" +cat > "$PINDIR/dotnet" <<'STUB' +#!/bin/bash +if [ "$1" = "--version" ]; then + # Model a pin that silently does not take effect. + if [ -f "$PWD/global.json" ]; then echo "9.0.100"; else echo "10.0.100"; fi + exit 0 +fi +if [ "$1" = "new" ] && [ "$2" = "globaljson" ]; then + printf '{"sdk":{"version":"x"}}' > "$PWD/global.json"; exit 0 +fi +case "$1" in + --list-sdks) echo "10.0.100 [$(dirname "$0")/sdk]" ;; + workload) echo "REACHED_INSTALL"; exit 0 ;; + *) exit 0 ;; +esac +STUB +chmod +x "$PINDIR/dotnet" + +if curl -sSf -m 20 -o /dev/null https://api.nuget.org/v3/index.json 2>/dev/null; then + pin_out="$(cd "$TMPROOT" && bash "$SH_SCRIPT" -d "$PINDIR" 2>&1)"; pin_rc=$? + if [[ $pin_rc -ne 0 ]] && grep -q "pin did not take effect" <<< "$pin_out" && ! grep -q "REACHED_INSTALL" <<< "$pin_out"; then + printf " %sPASS%s ineffective SDK pin aborts before install\n" "$c_green" "$c_reset" + pass=$((pass + 1)) + else + printf " %sFAIL%s ineffective SDK pin did not abort (exit %s)\n" "$c_red" "$c_reset" "$pin_rc" + echo "$pin_out" | tail -5 | sed 's/^/ | /' + fail=$((fail + 1)) + fi + + # A pin that DOES take effect must install normally. + PINOK="$TMPROOT/pinok" + mkdir -p "$PINOK" + cat > "$PINOK/dotnet" <<'STUB' +#!/bin/bash +case "$1" in + --version) echo "10.0.100" ;; + --list-sdks) echo "10.0.100 [$(dirname "$0")/sdk]" ;; + new) exit 0 ;; + workload) exit 0 ;; + *) exit 0 ;; +esac +STUB + chmod +x "$PINOK/dotnet" + ok_out="$(cd "$TMPROOT" && bash "$SH_SCRIPT" -d "$PINOK" 2>&1)"; ok_rc=$? + if [[ $ok_rc -eq 0 ]] && grep -q "^DONE$" <<< "$ok_out"; then + printf " %sPASS%s effective SDK pin installs normally\n" "$c_green" "$c_reset" + pass=$((pass + 1)) + else + printf " %sFAIL%s effective SDK pin failed (exit %s)\n" "$c_red" "$c_reset" "$ok_rc" + echo "$ok_out" | tail -5 | sed 's/^/ | /' + fail=$((fail + 1)) + fi +else + printf " %sSKIP%s SDK pin verification (no network)\n" "$c_yellow" "$c_reset" +fi + +# --- 10. empty feed response must not install "latest" ------------------------- +# +# The NuGet v2 package endpoint serves the LATEST version when the URL carries no version +# segment, so an empty resolved version must never reach the download step. + +echo "" +echo "-- empty version never reaches the download URL --" + +if grep -q 'Refusing to install: resolved an empty manifest id/version' "$SH_SCRIPT"; then + printf " %sPASS%s workload-install.sh guards against an empty resolved version\n" "$c_green" "$c_reset" + pass=$((pass + 1)) +else + printf " %sFAIL%s workload-install.sh has no empty-version guard\n" "$c_red" "$c_reset" + fail=$((fail + 1)) +fi + +if command -v pwsh >/dev/null 2>&1; then + # An empty versions[] must NOT yield a truthy "=" result. + cat > "$TMPROOT/empty-versions.ps1" <<'PSEOF' +param([string]$ScriptPath) +$src = Get-Content -Raw $ScriptPath +if ($src -match 'Where-Object \{ \$_ -and \$_\.Trim\(\) \} \| Select-Object -Last 1') { + Write-Output 'GUARDED' +} else { + Write-Output 'UNGUARDED' +} +if ($src -match 'IsNullOrWhiteSpace\(\$ResolvedVersion\)') { + Write-Output 'CALLER_GUARDED' +} else { + Write-Output 'CALLER_UNGUARDED' +} +PSEOF + ev="$(pwsh -NoProfile -File "$TMPROOT/empty-versions.ps1" -ScriptPath "$PS1_SCRIPT" 2>/dev/null | tr -d '\r')" + for want in GUARDED CALLER_GUARDED; do + if grep -Fqx "$want" <<< "$ev"; then + printf " %sPASS%s ps1 %s\n" "$c_green" "$c_reset" "$want" + pass=$((pass + 1)) + else + printf " %sFAIL%s ps1 missing %s\n" "$c_red" "$c_reset" "$want" + fail=$((fail + 1)) + fi + done +fi + echo "" echo "============= install-failure summary =============" echo " passed: $pass" diff --git a/workload/scripts/test-matrix.sh b/workload/scripts/test-matrix.sh index eab40b9e2..6e41ae753 100644 --- a/workload/scripts/test-matrix.sh +++ b/workload/scripts/test-matrix.sh @@ -254,7 +254,11 @@ if [[ -z "$ONLY" && $pass_count -gt 0 ]]; then rm -rf "$sc_dir" && mkdir -p "$sc_dir" # Reuse whichever row built successfully; any Tizen project will do. src_row="$(find "$TMPDIR" -maxdepth 1 -name 'net*-tizen*' -type d | head -1)" - if [[ -n "$src_row" ]]; then + if [[ -z "$src_row" ]]; then + fail "self-contained disposition could not run (no built row to reuse)" + fail_count+=1 + failed_rows+=("selfcontained:no-fixture") + else cp "$src_row/TizenApp1.csproj" "$src_row/tizen-manifest.xml" "$sc_dir/" 2>/dev/null cp -r "$src_row"/*.cs "$sc_dir/" 2>/dev/null sc_log="$sc_dir/selfcontained.log" @@ -271,8 +275,13 @@ if [[ -z "$ONLY" && $pass_count -gt 0 ]]; then fail_count+=1 failed_rows+=("selfcontained:xmlexception") else - warn "self-contained failed before reaching the Tizen guard (see $sc_log)" - grep -m2 "error" "$sc_log" | sed 's/^/ | /' + # Any other diagnostic is a FAILURE, not a warning. Self-contained has exactly one + # supported outcome; NETSDK1083 or anything else means the guard did not fire and + # the user gets an unactionable error. + fail "self-contained produced an unexpected diagnostic (expected TIZENSDK001)" + grep -m3 "error" "$sc_log" | sed 's/^/ | /' + fail_count+=1 + failed_rows+=("selfcontained:unexpected-diagnostic") fi fi fi diff --git a/workload/scripts/test-package-fallback.sh b/workload/scripts/test-package-fallback.sh index f48f22e43..e1d3814e2 100755 --- a/workload/scripts/test-package-fallback.sh +++ b/workload/scripts/test-package-fallback.sh @@ -108,6 +108,110 @@ for case in "${CASES[@]}"; do fi done +# --- selection priority + atomicity ------------------------------------------ +# +# PackageTargetFallback is an ORDERED preference list, but FixupNuGetReferences used to add +# every matching directory to an unordered HashSet (populated in FILESYSTEM enumeration +# order) and then take assemblies first-wins across all of them. Two consequences: +# * the declared priority was ignored whenever it disagreed with directory order, and +# * assemblies could be MIXED across TFMs within one package. +# +# The cases below are chosen so alphabetical directory order DISAGREES with the declared +# priority - otherwise the old implementation passes by luck. + +echo "" +echo "-- selection priority / atomicity --" + +TASK_PROJ="$WORKLOAD_DIR/src/Samsung.Tizen.Build.Tasks/Samsung.Tizen.Build.Tasks.csproj" +TASK_DLL="$WORKLOAD_DIR/src/Samsung.Tizen.Build.Tasks/bin/Release/netstandard2.0/Samsung.Tizen.Build.Tasks.dll" + +if [[ ! -f "$TASK_DLL" ]]; then + "$DOTNET" build "$TASK_PROJ" -c Release --nologo -v:q >/dev/null 2>&1 || true +fi + +# "