Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions .github/workflows/fw-lite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json
- name: Cache NuGet packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
Expand Down Expand Up @@ -123,7 +123,7 @@ jobs:
submodules: true
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json

- name: Dotnet build (Release)
run: dotnet build backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj -c Release
Expand Down Expand Up @@ -300,7 +300,7 @@ jobs:
path: ${{ env.VIEWER_BUILD_OUTPUT_DIR }}
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json

- name: Dotnet build
working-directory: backend/FwLite/FwLiteWeb
Expand Down Expand Up @@ -335,7 +335,7 @@ jobs:
path: ${{ env.VIEWER_BUILD_OUTPUT_DIR }}
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json

- name: Publish Linux
working-directory: backend/FwLite/FwLiteWeb
Expand Down Expand Up @@ -383,7 +383,7 @@ jobs:
path: ${{ env.VIEWER_BUILD_OUTPUT_DIR }}
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json

- name: Setup Maui
run: dotnet workload install maui-android
Expand All @@ -404,7 +404,7 @@ jobs:
KEYSTORE_PASS: ${{ secrets.FW_LITE_KEYSTORE_PASS }}
KEYSTORE_ALIAS: ${{ vars.FW_LITE_KEYSTORE_UPLOAD_KEY_ALIAS }}
run: |
dotnet publish -f net10.0-android -p:BuildApple=false --artifacts-path ../artifacts \
dotnet publish -f net11.0-android -p:BuildApple=false --artifacts-path ../artifacts \
-p:ApplicationDisplayVersion=${{ needs.build-and-test.outputs.semver-version }} \
-p:ApplicationVersion=${{ github.run_number }} \
-p:InformationalVersion=${{ needs.build-and-test.outputs.version }} \
Expand All @@ -421,7 +421,7 @@ jobs:
working-directory: backend/FwLite/FwLiteMaui
run: |
echo "::warning::Signing key unavailable (likely a fork PR); building an unsigned Android APK."
dotnet publish -f net10.0-android -p:BuildApple=false --artifacts-path ../artifacts \
dotnet publish -f net11.0-android -p:BuildApple=false --artifacts-path ../artifacts \
-p:ApplicationDisplayVersion=${{ needs.build-and-test.outputs.semver-version }} \
-p:ApplicationVersion=${{ github.run_number }} \
-p:InformationalVersion=${{ needs.build-and-test.outputs.version }} \
Expand All @@ -432,8 +432,8 @@ jobs:
with:
name: fw-lite-android
if-no-files-found: error
# path looks like this: backend/FwLite/artifacts/publish/FwLiteMaui/release_net10.0-android/org.sil.fwlitemaui-signed.apk
path: backend/FwLite/artifacts/publish/FwLiteMaui/release_net10.0-android/*
# path looks like this: backend/FwLite/artifacts/publish/FwLiteMaui/release_net11.0-android/org.sil.fwlitemaui-signed.apk
path: backend/FwLite/artifacts/publish/FwLiteMaui/release_net11.0-android/*

publish-win:
name: Publish FW Lite app for Windows
Expand All @@ -449,7 +449,7 @@ jobs:
path: ${{ env.VIEWER_BUILD_OUTPUT_DIR }}
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json

- name: Cache NuGet packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
Expand Down Expand Up @@ -482,7 +482,7 @@ jobs:
- name: Publish Windows MAUI portable app
working-directory: backend/FwLite/FwLiteMaui
run: |
dotnet publish -f net10.0-windows10.0.19041.0 -p:BuildAndroid=false --artifacts-path ../artifacts -p:WindowsPackageType=None -p:ApplicationDisplayVersion=${{ needs.build-and-test.outputs.semver-version }} -p:InformationalVersion=${{ needs.build-and-test.outputs.version }}
dotnet publish -f net11.0-windows10.0.19041.0 -p:BuildAndroid=false --artifacts-path ../artifacts -p:WindowsPackageType=None -p:ApplicationDisplayVersion=${{ needs.build-and-test.outputs.semver-version }} -p:InformationalVersion=${{ needs.build-and-test.outputs.version }}
mkdir -p ../artifacts/sign/portable
cp -r ../artifacts/publish/FwLiteMaui/* ../artifacts/sign/portable/

Expand All @@ -492,7 +492,7 @@ jobs:
# AppxPackageDir is the MSIX-targets equivalent of --artifacts-path: without it
# MAUI 10 writes AppPackages to $(MSBuildProjectDirectory)/AppPackages, ignoring
# --artifacts-path. Redirect it under artifacts/ so everything lives in one tree.
dotnet publish -f net10.0-windows10.0.19041.0 -p:BuildAndroid=false --artifacts-path ../artifacts -p:AppxPackageDir=../artifacts/AppPackages/ -p:ApplicationDisplayVersion=${{ needs.build-and-test.outputs.semver-version }} -p:InformationalVersion=${{ needs.build-and-test.outputs.version }}
dotnet publish -f net11.0-windows10.0.19041.0 -p:BuildAndroid=false --artifacts-path ../artifacts -p:AppxPackageDir=../artifacts/AppPackages/ -p:ApplicationDisplayVersion=${{ needs.build-and-test.outputs.semver-version }} -p:InformationalVersion=${{ needs.build-and-test.outputs.version }}
mkdir -p ../artifacts/msix
cp ../artifacts/AppPackages/*/*.msix ../artifacts/msix/

Expand Down Expand Up @@ -609,7 +609,7 @@ jobs:
path: ${{ env.VIEWER_BUILD_OUTPUT_DIR }}
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json
- name: Publish FwLiteWeb (linux-x64)
working-directory: backend/FwLite/FwLiteWeb
run: dotnet publish -r linux-x64 --artifacts-path ../artifacts -p:PublishSingleFile=true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration-test-gha.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json
- uses: ./.github/actions/setup-k8s
with:
lexbox-api-tag: ${{ inputs.lexbox-api-tag }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json
- uses: MatteoH2O1999/setup-python@429b7dee8a48c31eb72ce0b420ea938ff51c2f11 # v3.2.1
id: python
if: ${{ inputs.runs-on != 'windows-latest' && !env.act && inputs.hg-version == '3' }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lexbox-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json
- name: Install Task
uses: arduino/setup-task@c0bc642852239c2689f73f4ea6459c29405f3c52 # v3.0.0
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lexbox-fw-headless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.x'
global-json-file: global.json
- name: Dotnet build
run: dotnet build backend/FwHeadless/FwHeadless.csproj
- name: Unit tests
Expand Down
2 changes: 1 addition & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"build",
"${workspaceFolder}/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj",
"--framework",
"net10.0-windows10.0.19041.0",
"net11.0-windows10.0.19041.0",
],
"problemMatcher": "$msCompile"
},
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This is a monorepo containing:

### Tech Stack

- **Backend**: .NET 10, C#, Entity Framework Core, GraphQL (Hot Chocolate)
- **Backend**: .NET 11, C#, Entity Framework Core, GraphQL (Hot Chocolate)
- **Frontend**: SvelteKit, TypeScript
- **Database**: PostgreSQL
- **Infrastructure**: Docker, Kubernetes, Skaffold, Tilt
Expand Down Expand Up @@ -117,7 +117,7 @@ If you are struggling, explain the difficulty to the user instead of cheating. *

## Cursor Cloud specific instructions

System deps (.NET 10 SDK, Taskfile/`task`) are baked into the VM snapshot; the startup update script only refreshes pnpm + FwLiteWeb NuGet deps. The generated TS types under `frontend/viewer/src/lib/dotnet-types/generated-types/` are committed, so the viewer runs without a backend build.
System deps (.NET 11 SDK, Taskfile/`task`) are baked into the VM snapshot; the startup update script only refreshes pnpm + FwLiteWeb NuGet deps. The generated TS types under `frontend/viewer/src/lib/dotnet-types/generated-types/` are committed, so the viewer runs without a backend build.

### Running FW Lite Web (headless)

Expand Down
6 changes: 3 additions & 3 deletions backend/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Backend

.NET 10 backend for LexBox and FwLite applications.
.NET 11 backend for LexBox and FwLite applications.

## Build & Test

Expand All @@ -13,7 +13,7 @@ dotnet test

# Build specific project
dotnet build LexBoxApi/LexBoxApi.csproj
dotnet build FwLite/FwLiteMaui/FwLiteMaui.csproj --framework net10.0-windows10.0.19041.0
dotnet build FwLite/FwLiteMaui/FwLiteMaui.csproj --framework net11.0-windows10.0.19041.0
```

## Project Structure
Expand All @@ -38,7 +38,7 @@ To build against local Harmony source (e.g. when developing the CRDT substrate),

- **Nullable**: Enabled globally, `Nullable` warnings are errors
- **Implicit usings**: Enabled
- **Target framework**: net10.0 (unless platform-specific)
- **Target framework**: net11.0 (unless platform-specific)
- **Async**: Use `async/await`, not `.Result` or `.Wait()`. Prefer `return await Foo()` over returning the task directly — awaiting keeps this method on the stack trace when the inner call throws. Only drop the `await` (return the task) as a hot-path micro-optimization ([#2435](https://github.com/sillsdev/languageforge-lexbox/pull/2435#discussion_r3584331713))
- **Records**: Prefer for DTOs and immutable data

Expand Down
2 changes: 1 addition & 1 deletion backend/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</PropertyGroup>
<PropertyGroup>
<InformationalVersion>dev</InformationalVersion>
<TargetFramework Condition="'$(TargetFramework)' == ''">net10.0</TargetFramework>
<TargetFramework Condition="'$(TargetFramework)' == ''">net11.0</TargetFramework>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand Down
4 changes: 2 additions & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:11.0-preview AS build

COPY . .
# WORKDIR /src
Expand Down
42 changes: 36 additions & 6 deletions backend/FwHeadless/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:11.0-preview AS build

COPY . .
RUN --mount=type=cache,target=/root/.nuget/packages dotnet restore "FwHeadless/FwHeadless.csproj"
Expand All @@ -19,28 +19,58 @@ RUN --mount=type=cache,target=/root/.nuget/packages dotnet publish /p:Informatio

FROM mcr.microsoft.com/dotnet/runtime:8.0 AS dotnet8

# Chorus Mercurial 6.5 needs Python 3.12. aspnet:11.0-preview is Ubuntu 26.04
# (python3 is 3.14). Build 3.12 in a throwaway stage (same base, so glibc/ssl match);
# curl/gnupg/deadsnakes never enter the runtime image.
FROM base AS python312
RUN apt-get update \
&& apt-get install --yes --no-install-recommends ca-certificates curl gnupg \
&& curl -fsSL 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xF23C5A6CF475977595C89F51BA6932366A755776' \
| gpg --dearmor -o /etc/apt/keyrings/deadsnakes.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/deadsnakes.gpg] https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") main" \
> /etc/apt/sources.list.d/deadsnakes.list \
&& apt-get update \
&& apt-get install --yes --no-install-recommends python3.12 \
&& rm -rf /var/lib/apt/lists/*

FROM base AS final
RUN mkdir -p /var/lib/fw-headless /var/www/.local/share && chown -R www-data:www-data /var/lib/fw-headless /var/www/.local/share
# Shared libs python3.12 / stdlib extensions need; the aspnet image does not ship them.
# Installing python3.12 itself here would also pull distro python3 (3.14).
RUN apt-get update \
&& apt-get install --yes --no-install-recommends tini iputils-ping python3 \
&& apt-get install --yes --no-install-recommends tini iputils-ping ca-certificates \
libexpat1 zlib1g libbz2-1.0 libcrypt1 libdb5.3t64 libffi8 liblzma5 \
libncursesw6 libreadline8t64 libsqlite3-0 libssl3t64 libtinfo6 libuuid1 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=python312 /usr/bin/python3.12 /usr/bin/python3.12
COPY --from=python312 /usr/lib/python3.12 /usr/lib/python3.12
COPY --from=python312 /etc/python3.12 /etc/python3.12
WORKDIR /app
# Mercurial 6.5's demandimport breaks on 3.12+ (circular import of threading.RLock).
# Chorus spawns hg as a child, so this must be in the image env.
ENV HGDEMANDIMPORT=disable
# dotnet 8 runtime is for ChorusMerge
COPY --from=dotnet8 /usr/share/dotnet/host/fxr/ /usr/share/dotnet/host/fxr/
COPY --from=dotnet8 /usr/share/dotnet/shared/Microsoft.NETCore.App/ /usr/share/dotnet/shared/Microsoft.NETCore.App/
COPY --from=publish /app/publish .
# Ensure Chorus and Mercurial exec bits were not stripped by dotnet CLI tools
RUN chmod +x chorusmerge && chmod +x Mercurial/hg && chmod +x Mercurial/chg 2>/dev/null || true
# Fix up mercurial.ini path to fixutf8
RUN sed -i -e 's/fixutf8 = \/FwHeadless/fixutf8 = \/app/' Mercurial/mercurial.ini
# Fix up mercurial.ini path to fixutf8; point hg at python3.12 (shebang is python3)
RUN sed -i -e 's/fixutf8 = \/FwHeadless/fixutf8 = \/app/' Mercurial/mercurial.ini \
&& sed -i '1s|^#!.*python3.*|#!/usr/bin/env python3.12|' Mercurial/hg
# Smoke-test: bundled hg must start (python3.12). Chorus treats hg exit 1 as
# success, so a dead interpreter looks like "no .hg" at clone time.
RUN python3.12 --version \
&& ./Mercurial/hg version \
|| { echo "hg version smoke test failed (python3.12 $(python3.12 --version 2>&1))"; exit 1; }
COPY Testing/test-template-repo.zip /tmp/test-template-repo.zip
# Smoke-test FixFwData at image build time: FwHeadless runs under dotnet (AspNetCore shared
# framework) but FixFwData is a standalone apphost (Core only). Web publish does not copy the
# extra assemblies FixFwData needs (e.g. Logging.Abstractions); the csproj PublishFixFwData
# target must publish it into the same output. Running it here catches a broken publish early.
RUN test -f FixFwData \
&& chmod +x FixFwData \
&& python3 -c "import zipfile; open('/tmp/smoke.fwdata','wb').write(zipfile.ZipFile('/tmp/test-template-repo.zip').read('kevin-test-01.fwdata'))" \
&& python3.12 -c "import zipfile; open('/tmp/smoke.fwdata','wb').write(zipfile.ZipFile('/tmp/test-template-repo.zip').read('kevin-test-01.fwdata'))" \
&& ./FixFwData /tmp/smoke.fwdata; ec=$?; rm -f /tmp/test-template-repo.zip /tmp/smoke.fwdata; \
if [ $ec -gt 1 ]; then echo "FixFwData smoke test failed with exit $ec"; exit 1; fi
# Smoke-test: chorusmerge must engage the FieldWorks merge handler, not silently keep one
Expand Down
2 changes: 1 addition & 1 deletion backend/FwHeadless/FwHeadless.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
<Link>chorusmerge</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<!-- Do not update this net8.0 path when we change frameworks; instead, this should be updated if/when Chorus acquires a net10.0 build -->
<!-- Do not update this net8.0 path when we change frameworks; instead, this should be updated if/when Chorus acquires a net11.0 build -->
<None Include="$(PkgSIL_Chorus_ChorusMerge)\lib\net8.0\ChorusMerge.runtimeconfig.json">
<Link>ChorusMerge.runtimeconfig.json</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand Down
4 changes: 3 additions & 1 deletion backend/FwLite/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ task fw-lite-web # from repo root
dotnet test FwLiteOnly.slnf

# Build MAUI app (Windows)
dotnet build FwLiteMaui/FwLiteMaui.csproj --framework net10.0-windows10.0.19041.0
dotnet build FwLiteMaui/FwLiteMaui.csproj --framework net11.0-windows10.0.19041.0
```

## Testing on Android (agents)
Expand All @@ -48,6 +48,8 @@ task fw-lite:has-stale-generated-types

The configuration for this lives in `FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs` and `FwLiteShared/Reinforced.Typings.settings.xml`.

> **.NET 11 preview note:** Reinforced.Typings 1.6.7 has no net11 tool, so `FwLiteShared.csproj` forces its net10 `rtcli` and patches the tool's runtimeconfig to run on the net11 runtime (`_RtPatchToolRuntimeConfig` target). It's self-contained — no env vars needed. Delete `RtForceTargetFramework` + that target once the package ships a net11 tool.

⚠️ **`[JSInvokable]` methods must not have optional/defaulted parameters (nor a `CancellationToken`).** The generated TS marks them optional (`arg?`), but that's a lie: Blazor JSInterop requires JS to pass every parameter, and can't marshal a `CancellationToken` — so JS callers break at runtime. If a server-side (REST) caller needs cancellation or an extra arg, add a separate **non-`[JSInvokable]`** overload for it (see `AuthService.SignInWebView`).

## Project Structure
Expand Down
8 changes: 4 additions & 4 deletions backend/FwLite/FwLiteMaui.Tests/FwLiteMaui.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
<IsTestProject>true</IsTestProject>
<!-- override the target framework set in Directory.Build.props, we will use what is defined in TargetFrameworks below instead-->
<TargetFramework/>
<TargetFrameworks Condition=" '$(BuildAndroid)' != 'false' ">$(TargetFrameworks);net10.0-android</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('osx')) And '$(BuildApple)' != 'false'">$(TargetFrameworks);net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
<NoWarn Condition="'$(TargetFramework)' == 'net10.0-android'">$(NoWarn);XA0101</NoWarn>
<TargetFrameworks Condition=" '$(BuildAndroid)' != 'false' ">$(TargetFrameworks);net11.0-android</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('osx')) And '$(BuildApple)' != 'false'">$(TargetFrameworks);net11.0-ios;net11.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net11.0-windows10.0.19041.0</TargetFrameworks>
<NoWarn Condition="'$(TargetFramework)' == 'net11.0-android'">$(NoWarn);XA0101</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading
Loading