build: the exe is LlamaCasty.exe, and the publish script proves the artifact
Renames AssemblyName to LlamaCasty before the first Steam depot — Steamworks is explicit that renaming a live exe is painful, so this had to land first, not later. Four assembly-qualified pack URIs carried the old name, not three: MainWindow (window icon), OverlayHost (logo), RenameRecordingDialog and KeepBroadcastDialog. Pack URIs resolve by assembly name at runtime, so a stale one compiles, passes every test and then silently renders nothing the first time that view opens. AssemblyIdentityTests guards both halves — the assembly name, and no source XAML still naming the legacy assembly. Packaging/Package.appxmanifest and package-msix.sh carry the exe name too. scripts/publish.sh replaces the by-hand publish: single-file win-x64, ReadyToRun, ffmpeg beside the exe, and it refuses to finish if ffmpeg, LICENSE, PRIVACY.md or THIRD-PARTY-NOTICES.txt are missing, or if a test assembly leaked in. That last guard exists because the first publish was wrong in a way no test could see: PublishSingleFile + IncludeNativeLibrariesForSelfExtract bundled the libav*.dll family into the exe — a 9-file output with no ffmpeg folder and a 182 MB binary, so the locator would have found nothing at runtime. ExcludeFromSingleFile="true" on the content keeps the folder a folder. Diffing against a control publish without the single-file flags found it; guessing at item globs wasted two attempts first (recorded in MyMistakes.md). Measured: 127 MB exe + 129 MB ffmpeg = 257 MB depot. Tests: AssemblyIdentity, StorePackaging, BuildStamp, BundledFonts all pass; app builds with 0 warnings.
This commit is contained in:
@@ -156,7 +156,7 @@
|
|||||||
|
|
||||||
<!-- Hub view -->
|
<!-- Hub view -->
|
||||||
<StackPanel Visibility="{Binding IsHubVisible, Converter={StaticResource BoolToVis}}">
|
<StackPanel Visibility="{Binding IsHubVisible, Converter={StaticResource BoolToVis}}">
|
||||||
<Image Source="/ytLive;component/Assets/llamacasty-logo.jpg" Height="125" Stretch="Uniform"
|
<Image Source="/LlamaCasty;component/Assets/llamacasty-logo.jpg" Height="125" Stretch="Uniform"
|
||||||
HorizontalAlignment="Center" Margin="0,0,0,12"/>
|
HorizontalAlignment="Center" Margin="0,0,0,12"/>
|
||||||
<TextBlock Text="{Binding AppVersionLabel}" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12"
|
<TextBlock Text="{Binding AppVersionLabel}" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12"
|
||||||
HorizontalAlignment="Center" Margin="0,4,0,0"/>
|
HorizontalAlignment="Center" Margin="0,4,0,0"/>
|
||||||
|
|||||||
+7
-3
@@ -28,9 +28,13 @@ dotnet publish ytLive.csproj -c Release -r win-x64 --self-contained true \
|
|||||||
read DLC ownership. Its absence must degrade to the free profile, never to a crash.
|
read DLC ownership. Its absence must degrade to the free profile, never to a crash.
|
||||||
- **WebView2 is the one machine dependency.** Valve's Common Redistributables configure the
|
- **WebView2 is the one machine dependency.** Valve's Common Redistributables configure the
|
||||||
Evergreen bootstrapper as a prerequisite, so the depot declares it instead of us shipping it.
|
Evergreen bootstrapper as a prerequisite, so the depot declares it instead of us shipping it.
|
||||||
- **`AssemblyName` is still `ytLive`** → the exe is `ytLive.exe` today. The rename to
|
- **`AssemblyName` is `LlamaCasty`** (renamed 2026-11-01, before the first depot because
|
||||||
`LlamaCasty.exe` is queued (three pack URIs break — see `product.md`), and **must land before the
|
Steamworks warns about renaming a live exe) → the artifact is `LlamaCasty.exe` plus its
|
||||||
first depot upload**, because Steam's docs are explicit that renaming a live exe is painful.
|
`ffmpeg\` folder. `scripts/publish.sh` stages both and refuses to finish if either is missing.
|
||||||
|
- **⛔ `ExcludeFromSingleFile="true"` on the ffmpeg content.** Without it, `PublishSingleFile` +
|
||||||
|
`IncludeNativeLibrariesForSelfExtract` swallow the libav\*.dll family **into the exe** — a
|
||||||
|
182 MB binary with no `ffmpeg\` folder beside it, so the locator finds nothing and every stream
|
||||||
|
fails at runtime. The publish output is ~257 MB: 127 MB exe + 129 MB ffmpeg.
|
||||||
- **Portable mode** (`portable.txt` → `.\data\`) survives as a feature for a copied install; it is
|
- **Portable mode** (`portable.txt` → `.\data\`) survives as a feature for a copied install; it is
|
||||||
not a delivery vector.
|
not a delivery vector.
|
||||||
|
|
||||||
|
|||||||
+23
-18
@@ -1,35 +1,40 @@
|
|||||||
# HANDOFF — 2026-10-31 (Steam is the only channel; the site says so)
|
# HANDOFF — 2026-11-01 (the exe is LlamaCasty.exe; the depot folder builds and verifies)
|
||||||
|
|
||||||
## State
|
## State
|
||||||
|
|
||||||
- Branch `main`, level with `origin` through `57ef9e7`; this unit is local.
|
- Branch `main`, level with `origin` through `8f79b20`; this unit (the artifact rename) is local.
|
||||||
- **Distribution is Steam-exclusive** (creator ruling 2026-10-31): a free app on Steam with the paid
|
- **The shipped exe is `LlamaCasty.exe`.** `AssemblyName` renamed; four pack URIs fixed (the docs
|
||||||
edition as a **Steam DLC**; the **$10/month** plan is sold off-Steam by Polar and *stated, never
|
said three — `KeepBroadcastDialog.xaml:5` was the fourth); manifest, MSIX script and
|
||||||
sold*, inside a Steam build. Winget, the direct zip, the Store and the certificate that served
|
`StorePackagingTests` follow. Guarded by `ytLive.Tests/AssemblyIdentityTests.cs`.
|
||||||
them are retired. `commercial.md` → Channels owns policy, `Distribution.md` the depot mechanics,
|
- **`scripts/publish.sh`** stages `artifacts/steam-stage` and refuses to finish if `ffmpeg\`, the
|
||||||
`decisions.md` the cause of death.
|
licence, privacy or notices file is missing, or test assemblies leaked in. Output 257 MB.
|
||||||
- **Entitlement is `steamDlcOwned || polarKeyValid`**, both resolving to one `OutputProfile`.
|
- **Distribution is Steam-exclusive** (creator ruling 2026-10-31): free app + paid edition as a
|
||||||
`steam_api64.dll` ships beside the exe; the app must start without Steam and fall back to free.
|
Steam **DLC**; the **$10/month** plan sold off-Steam by Polar and *stated, never sold*, in-app.
|
||||||
- **Site updated and deployed** (`a9d07ce`): "Coming soon to Steam" ×6, zero Microsoft references,
|
`commercial.md` → Channels is policy; `Distribution.md` is depot mechanics.
|
||||||
privacy page says Steam bills the paid edition. The CTA gains a wishlist button only when the
|
- **Entitlement is `steamDlcOwned || polarKeyValid`**; `steam_api64.dll` ships beside the exe and
|
||||||
store page is public.
|
the app must start without Steam, falling back to free.
|
||||||
- `www.llamacasty.com` resolves and 301s to the apex. Recipe in `MyMistakes.md`.
|
- **Site is live with Steam** (`a9d07ce`). `www.llamacasty.com` 301s to the apex.
|
||||||
|
|
||||||
## Next action
|
## Next action
|
||||||
|
|
||||||
1. **Steamworks onboarding** (creator is in it now): NDA → identity → company details → tax
|
1. **OutputProfile gate** — ≤921,600 px / ≤30 fps for the stream **and** the recorder, greyed tier
|
||||||
interview → payout bank → **$100 Steam Direct** → create the base app, note the App ID.
|
entries, plus the TASK 53 geometry tests that pin every tier.
|
||||||
2. Then the surge slice: artifact shape (rename + publish + `steam_api64.dll`) → OutputProfile gate
|
2. Then the **entitlement seam** (`IEntitlement`, Steam DLC path behind a fake, Polar path) and the
|
||||||
+ TASK 53 geometry tests → entitlement seam → licence UI + a Steam-less launch test.
|
licence UI.
|
||||||
|
3. Steamworks: App ID waiting on identity verification; then depot, store page (public 2 weeks
|
||||||
|
before release) and the DLC app.
|
||||||
|
|
||||||
## Landmines
|
## Landmines
|
||||||
|
|
||||||
|
- **⛔ `ExcludeFromSingleFile="true"` on the ffmpeg content** — without it the publisher bundles
|
||||||
|
libav\*.dll into the exe and the app ships unable to find ffmpeg. Recipe in `MyMistakes.md`.
|
||||||
- **Nothing may gate a feature.** The only paid delta is the output profile; `ShowBrandFlash` stays
|
- **Nothing may gate a feature.** The only paid delta is the output profile; `ShowBrandFlash` stays
|
||||||
`false` and entitlement must never enable it.
|
`false` and entitlement must never enable it.
|
||||||
- **No checkout inside a Steam build** — the Wallet rule. The monthly is named, never sold, in-app.
|
- **No checkout inside a Steam build** — the Wallet rule. The monthly is named, never sold, in-app.
|
||||||
- **The org access token never ships.** `organization_id` is public; the token is not.
|
- **The org access token never ships.** `organization_id` is public; the token is not.
|
||||||
- **The site publishes the model, never the numbers**, and never takes payment or links a checkout.
|
- **The site publishes the model, never the numbers**, and never takes payment or links a checkout.
|
||||||
- **No self-updater and no version banner**: Steam updates the app.
|
- **No self-updater and no version banner**: Steam updates the app.
|
||||||
- Pre-existing: the **full test suite hangs** (WASAPI teardown). Per-class runs are the way.
|
- Pre-existing: the **full test suite hangs** (WASAPI teardown) — per-class runs only; and
|
||||||
|
`ytLive.Tests` carries 3 xUnit analyzer warnings the app-only gate does not see.
|
||||||
- Not every hex is a theme colour — `BorderColorPalette`/chat defaults are USER DATA; on-stream
|
- Not every hex is a theme colour — `BorderColorPalette`/chat defaults are USER DATA; on-stream
|
||||||
graphics come from `OutputPalette`. A red settings control is still a bug.
|
graphics come from `OutputPalette`. A red settings control is still a bug.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="Keep this broadcast?" Height="240" Width="460"
|
Title="Keep this broadcast?" Height="240" Width="460"
|
||||||
Icon="/ytLive;component/Assets/llamacasty-icon.png"
|
Icon="/LlamaCasty;component/Assets/llamacasty-icon.png"
|
||||||
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
|
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
|
||||||
ShowInTaskbar="False" Background="{StaticResource Brush.Background}">
|
ShowInTaskbar="False" Background="{StaticResource Brush.Background}">
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="{Binding WindowTitle}" Height="1040" Width="1920"
|
Title="{Binding WindowTitle}" Height="1040" Width="1920"
|
||||||
MinWidth="1366" MinHeight="768"
|
MinWidth="1366" MinHeight="768"
|
||||||
Icon="/ytLive;component/Assets/llamacasty-icon.png"
|
Icon="/LlamaCasty;component/Assets/llamacasty-icon.png"
|
||||||
Background="{StaticResource Brush.Background}"
|
Background="{StaticResource Brush.Background}"
|
||||||
FontFamily="{StaticResource FontFamily.Sans}"
|
FontFamily="{StaticResource FontFamily.Sans}"
|
||||||
WindowStartupLocation="Manual"
|
WindowStartupLocation="Manual"
|
||||||
|
|||||||
@@ -166,6 +166,35 @@ in Studio → Settings → Channel → **Feature eligibility**:
|
|||||||
*"Advanced YouTube features aren't available for this account"*. If ID verification is ever
|
*"Advanced YouTube features aren't available for this account"*. If ID verification is ever
|
||||||
wanted on a personal channel, do it **before** converting to a brand account.
|
wanted on a personal channel, do it **before** converting to a brand account.
|
||||||
|
|
||||||
|
### THE SINGLE-FILE PUBLISHER EATS ffmpeg (2026-11-01)
|
||||||
|
|
||||||
|
**Problem:** `dotnet publish` with `PublishSingleFile=true` +
|
||||||
|
`IncludeNativeLibrariesForSelfExtract=true` produced a **9-file** output with **no `ffmpeg\`
|
||||||
|
folder** and a **182 MB exe** — the libav\*.dll family had been bundled *inside* the binary. The
|
||||||
|
build was green, the tests passed, and the app would have shipped unable to find ffmpeg at
|
||||||
|
runtime (the locator looks for `ffmpeg\ffmpeg.exe` beside the exe).
|
||||||
|
|
||||||
|
**Diagnosis that worked:** diff the publish output against a **control publish with the
|
||||||
|
single-file flags removed** — ffmpeg was there. Then add flags back one at a time;
|
||||||
|
`IncludeNativeLibrariesForSelfExtract` was the one that broke it.
|
||||||
|
|
||||||
|
**Fix:** mark the content `ExcludeFromSingleFile="true"`.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<None Include="ffmpeg\**" CopyToOutputDirectory="PreserveNewest"
|
||||||
|
CopyToPublishDirectory="Always" ExcludeFromSingleFile="true" />
|
||||||
|
```
|
||||||
|
|
||||||
|
After: 127 MB exe, `ffmpeg\` present, depot ≈ 257 MB total.
|
||||||
|
|
||||||
|
- ⚠️ **The `<None Remove="ffmpeg\**" />` before it is not the fix** — it silences a duplicate-item
|
||||||
|
warning from the SDK's default glob, and I tried it first and wrongly believed it had worked.
|
||||||
|
Two failed remedies in a row is the research trigger: read the publisher's own metadata
|
||||||
|
(`ExcludeFromSingleFile`) instead of guessing at item globs.
|
||||||
|
- ⚠️ **A clean build and green tests cannot see this.** Only the artifact can, which is why
|
||||||
|
`scripts/publish.sh` fails loudly when `ffmpeg\`, `LICENSE`, `PRIVACY.md` or
|
||||||
|
`THIRD-PARTY-NOTICES.txt` are missing, and when test assemblies leak in.
|
||||||
|
|
||||||
### STEAM IS THE DISTRIBUTOR — AND WHAT IT DOES NOT ALLOW (2026-10-31)
|
### STEAM IS THE DISTRIBUTOR — AND WHAT IT DOES NOT ALLOW (2026-10-31)
|
||||||
|
|
||||||
**Why recorded:** the distribution answer changed three times in one day (winget → zip+GitHub →
|
**Why recorded:** the distribution answer changed three times in one day (winget → zip+GitHub →
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<Applications>
|
<Applications>
|
||||||
<Application
|
<Application
|
||||||
Id="LlamaCasty"
|
Id="LlamaCasty"
|
||||||
Executable="ytLive.exe"
|
Executable="LlamaCasty.exe"
|
||||||
EntryPoint="Windows.FullTrustApplication"
|
EntryPoint="Windows.FullTrustApplication"
|
||||||
uap10:TrustLevel="mediumIL"
|
uap10:TrustLevel="mediumIL"
|
||||||
uap10:RuntimeBehavior="packagedClassicApp">
|
uap10:RuntimeBehavior="packagedClassicApp">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="Rename Recording" Height="304" Width="460"
|
Title="Rename Recording" Height="304" Width="460"
|
||||||
Icon="/ytLive;component/Assets/llamacasty-icon.png"
|
Icon="/LlamaCasty;component/Assets/llamacasty-icon.png"
|
||||||
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
|
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
|
||||||
ShowInTaskbar="False" Background="{StaticResource Brush.Background}">
|
ShowInTaskbar="False" Background="{StaticResource Brush.Background}">
|
||||||
|
|
||||||
|
|||||||
@@ -7,22 +7,18 @@
|
|||||||
|
|
||||||
## Next up
|
## Next up
|
||||||
|
|
||||||
**The surge week (4 days, in this order).** Not every row below — this slice only, because it is
|
**The surge week (3 days, in this order).** Not every row below — this slice only, because it is
|
||||||
what the first public release is waiting on:
|
what the first public release is waiting on. Day 1 (artifact shape) is **done**: the exe is
|
||||||
|
`LlamaCasty.exe` and `scripts/publish.sh` stages the depot folder.
|
||||||
|
|
||||||
1. **Artifact shape** — `LlamaCasty.exe` rename (three pack URIs), `dotnet publish` profile with
|
1. **OutputProfile gate** — ≤921,600 px / ≤30 fps for the stream **and** the recorder, greyed tier
|
||||||
`steam_api64.dll` beside the exe. Ends with a build that starts standalone and falls back to the
|
|
||||||
free profile.
|
|
||||||
2. **OutputProfile gate** — ≤921,600 px / ≤30 fps for the stream **and** the recorder, greyed tier
|
|
||||||
entries, plus the TASK 53 geometry tests that pin every tier.
|
entries, plus the TASK 53 geometry tests that pin every tier.
|
||||||
3. **Entitlement seam** — `IEntitlement` with the Steam DLC path and a fake for tests, Polar's
|
2. Then the **entitlement seam** (`IEntitlement`, Steam DLC path behind a fake, Polar path) and the
|
||||||
customer-portal path for the monthly, `--activate` parsing, system messages.
|
licence UI.
|
||||||
4. **Real-machine day** — licence UI skeleton, a Steam-less launch proving the free profile, a test
|
|
||||||
broadcast.
|
|
||||||
|
|
||||||
Detail: [`commercial.md`](docs/ai/commercial.md) → Output profile / Entitlement;
|
Detail: [`commercial.md`](docs/ai/commercial.md) → Output profile / Entitlement;
|
||||||
[`Distribution.md`](Distribution.md). Day 5, if the four go well: the visibility picker (a hard
|
[`Distribution.md`](Distribution.md). After that: the visibility picker (a hard prerequisite for
|
||||||
prerequisite for any *public* build).
|
any *public* build).
|
||||||
|
|
||||||
**TASK 3·17 — the Text layer** stays the first product row after the surge. The `TextOverlay` source
|
**TASK 3·17 — the Text layer** stays the first product row after the surge. The `TextOverlay` source
|
||||||
type exists as a placeholder and nothing else does: `AddSource` creates it 0×0, all four compositor
|
type exists as a placeholder and nothing else does: `AddSource` creates it 0×0, all four compositor
|
||||||
@@ -80,9 +76,9 @@ Dog test.
|
|||||||
`UpdateBroadcast` in the same unit (it is the only code that can change a broadcast's privacy).
|
`UpdateBroadcast` in the same unit (it is the only code that can change a broadcast's privacy).
|
||||||
- **Kill the dev multi-instance affordance** — `Helpers/InstanceProfile.cs` is already `#if DEBUG`;
|
- **Kill the dev multi-instance affordance** — `Helpers/InstanceProfile.cs` is already `#if DEBUG`;
|
||||||
re-prove it on a Release build.
|
re-prove it on a Release build.
|
||||||
- **Release engineering** — `scripts/publish.sh` (self-contained win-x64, no test artifacts),
|
- **Release engineering** — `scripts/publish.sh` **done** (self-contained win-x64, `ffmpeg\`
|
||||||
Debug-only `InternalsVisibleTo`, the `LlamaCasty.exe` assembly rename (three pack URIs, incl.
|
guarded, no test artifacts), `LlamaCasty.exe` assembly rename **done**, full license texts beside
|
||||||
`MainWindow.xaml:14`), full license texts beside `THIRD-PARTY-NOTICES.txt` (TASK 4 req 9).
|
`THIRD-PARTY-NOTICES.txt` (TASK 4 req 9). Outstanding: Debug-only `InternalsVisibleTo`.
|
||||||
- **Gold pass** (TASK 36): the screens/layers audit, native-Windows verification, and the
|
- **Gold pass** (TASK 36): the screens/layers audit, native-Windows verification, and the
|
||||||
branding-flash decision.
|
branding-flash decision.
|
||||||
|
|
||||||
|
|||||||
+13
-9
@@ -28,18 +28,22 @@ the internal repo/assembly/namespace name only. Exact current split:
|
|||||||
| surface | name | note |
|
| surface | name | note |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| product / site / marketing | **LlamaCasty** | llamacasty.com, llamachile.tube |
|
| product / site / marketing | **LlamaCasty** | llamacasty.com, llamachile.tube |
|
||||||
| repo dir, csproj, `RootNamespace` | `ytLive` | 230 files: `namespace ytLive.*` |
|
| repo dir, csproj file, `RootNamespace` | `ytLive` | 230+ files: `namespace ytLive.*` |
|
||||||
| `AssemblyName` → shipped exe | `ytLive` | `ytLive.exe` |
|
| `AssemblyName` → shipped exe | **`LlamaCasty`** | `LlamaCasty.exe` — renamed 2026-11-01 |
|
||||||
| app data dir | `%APPDATA%\ytLlive` | **capital L**, drifts from `ytLive` |
|
| app data dir | `%APPDATA%\ytLlive` | **capital L**, drifts from `ytLive` |
|
||||||
| `Distribution.md` tells customers | `LlamaCasty.exe` | ⚠ **mismatch vs the real `ytLive.exe`** |
|
|
||||||
|
|
||||||
**Consequences, so nobody re-hunts them:**
|
**Consequences, so nobody re-hunts them:**
|
||||||
- Renaming `<AssemblyName>` to `LlamaCasty` is the cheap win (the customer-visible exe
|
- The assembly rename landed 2026-11-01 (before the first Steam depot, because Steamworks warns
|
||||||
name matches what `Distribution.md` already promises; namespaces can stay `ytLive.*`).
|
that renaming a live exe is painful). Namespaces stay `ytLive.*` — they are not user-visible
|
||||||
It is **4 lines**, but it hard-breaks three pack URIs that spell the assembly name:
|
and renaming them would churn every file for nothing.
|
||||||
`MainWindow.xaml:14` (the **window icon** — taskbar/Alt-Tab), `OverlayHost.xaml:96`,
|
- An assembly-qualified pack URI spells the assembly name, so the rename **hard-breaks silently**
|
||||||
`RenameRecordingDialog.xaml:5`. Miss those and you ship a broken icon. Queued in TASKS.md
|
at runtime, not at compile time. Four carried it, not three:
|
||||||
under the shipping task, not done.
|
`MainWindow.xaml:14` (window icon — taskbar/Alt-Tab), `OverlayHost.xaml:96`,
|
||||||
|
`RenameRecordingDialog.xaml:5`, and `KeepBroadcastDialog.xaml:5`.
|
||||||
|
`ytLive.Tests/AssemblyIdentityTests.cs` now guards both halves: the assembly name is
|
||||||
|
`LlamaCasty` and no source XAML still names the legacy assembly.
|
||||||
|
- `Packaging/Package.appxmanifest` (`Executable`) and `scripts/package-msix.sh` carry the exe
|
||||||
|
name too; the MSIX is unused but must not lie about the artifact.
|
||||||
- Renaming the `%APPDATA%\ytLlive` folder is **breaking** — it would orphan every existing
|
- Renaming the `%APPDATA%\ytLlive` folder is **breaking** — it would orphan every existing
|
||||||
user's layout DB, `Assets` and settings. Never do it casually; needs a migration.
|
user's layout DB, `Assets` and settings. Never do it casually; needs a migration.
|
||||||
- No code compares its own assembly name (no `GetExecutingAssembly().GetName().Name` checks),
|
- No code compares its own assembly name (no `GetExecutingAssembly().GetName().Name` checks),
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ mkdir -p "$STAGE_WSL"
|
|||||||
--nologo -v minimal \
|
--nologo -v minimal \
|
||||||
-o "$(wslpath -w "$STAGE_WSL")"
|
-o "$(wslpath -w "$STAGE_WSL")"
|
||||||
|
|
||||||
[ -f "$STAGE_WSL/ytLive.exe" ] || { echo "publish produced no ytLive.exe" >&2; exit 1; }
|
[ -f "$STAGE_WSL/LlamaCasty.exe" ] || { echo "publish produced no LlamaCasty.exe" >&2; exit 1; }
|
||||||
[ -d "$STAGE_WSL/ffmpeg" ] || {
|
[ -d "$STAGE_WSL/ffmpeg" ] || {
|
||||||
echo "ffmpeg/ is missing from the package — it must ship bundled (Store policy 10.2.2)." >&2
|
echo "ffmpeg/ is missing from the package — it must ship bundled (Store policy 10.2.2)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# publish.sh — build the artifact that goes into the Steam depot.
|
||||||
|
# Usage: ./scripts/publish.sh [output-dir] (default: artifacts/steam-stage)
|
||||||
|
#
|
||||||
|
# Why a script: the flags below are load-bearing and easy to lose. PublishTrimmed stays OFF
|
||||||
|
# (WPF resolves XAML by reflection), the single-file bundle must extract natives or ffmpeg-
|
||||||
|
# adjacent loading breaks, and ReadyToRun is what keeps startup under a second on a cold
|
||||||
|
# machine. Getting one of them wrong does not fail the build — it fails on a user's PC.
|
||||||
|
#
|
||||||
|
# The output is a folder, not a zip: steamcmd uploads a directory into a depot, and Steam
|
||||||
|
# delivers it. There is no installer, no admin prompt and no self-updater (Steam updates).
|
||||||
|
#
|
||||||
|
# Requires WINDOWS dotnet — see docs/ai/environment.md -> Run.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOTNET="/mnt/c/Program Files/dotnet/dotnet.exe"
|
||||||
|
[ -x "$DOTNET" ] || { echo "❌ Windows dotnet not found at $DOTNET"; exit 1; }
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
OUT_REL="${1:-artifacts/steam-stage}"
|
||||||
|
OUT="$ROOT/$OUT_REL"
|
||||||
|
|
||||||
|
echo "══ publish ══"
|
||||||
|
rm -rf "$OUT"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
"$DOTNET" publish "$(wslpath -w "$ROOT/ytLive.csproj")" \
|
||||||
|
-c Release -r win-x64 --self-contained true \
|
||||||
|
-p:PublishSingleFile=true \
|
||||||
|
-p:IncludeNativeLibrariesForSelfExtract=true \
|
||||||
|
-p:EnableCompressionInSingleFile=true \
|
||||||
|
-p:PublishReadyToRun=true \
|
||||||
|
-p:DebugType=none \
|
||||||
|
--nologo -v minimal \
|
||||||
|
-o "$(wslpath -w "$OUT")"
|
||||||
|
|
||||||
|
echo "══ verify the staged artifact ══"
|
||||||
|
fail() { echo "❌ $1" >&2; exit 1; }
|
||||||
|
|
||||||
|
[ -f "$OUT/LlamaCasty.exe" ] || fail "publish produced no LlamaCasty.exe (AssemblyName reverted?)"
|
||||||
|
[ -d "$OUT/ffmpeg" ] || fail "ffmpeg/ is missing — it must ship beside the exe (docs/ai/output.md)"
|
||||||
|
[ -f "$OUT/LICENSE" ] || fail "LICENSE is missing from the artifact"
|
||||||
|
[ -f "$OUT/PRIVACY.md" ] || fail "PRIVACY.md is missing from the artifact"
|
||||||
|
[ -f "$OUT/THIRD-PARTY-NOTICES.txt" ] || fail "THIRD-PARTY-NOTICES.txt is missing from the artifact"
|
||||||
|
|
||||||
|
# Test artifacts must never reach a depot: a shipped xunit host is a support ticket.
|
||||||
|
if compgen -G "$OUT/*.Tests.dll" > /dev/null || compgen -G "$OUT/xunit*" > /dev/null; then
|
||||||
|
fail "test assemblies leaked into the publish output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# steam_api64.dll arrives with the Steamworks SDK; until then its absence is expected and the
|
||||||
|
# app must fall back to the free profile, so this is a notice, not a failure.
|
||||||
|
if [ ! -f "$OUT/steam_api64.dll" ]; then
|
||||||
|
echo "⚠ steam_api64.dll not staged — the build will run as the free profile only."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
du -sh "$OUT" | awk '{print " artifact size: " $1}'
|
||||||
|
echo "✓ staged: $OUT_REL"
|
||||||
|
echo ""
|
||||||
|
echo "Next: steamcmd +run_app_build scripts/app_build_<appid>.vdf"
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ytLive.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The shipped binary is <c>LlamaCasty.exe</c> (renamed 2026-11-01, before the first Steam
|
||||||
|
/// depot). Two things have to agree for that to be true, and neither fails the build when it
|
||||||
|
/// doesn't:
|
||||||
|
///
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>The assembly name comes from <c>ytLive.csproj</c> and silently reverting it produces
|
||||||
|
/// <c>ytLive.exe</c> again — which is why the rename lands before a depot upload rather than
|
||||||
|
/// after (Steamworks: renaming a live exe is painful).</item>
|
||||||
|
/// <item>Assembly-qualified WPF pack URIs (<c>/Assembly;component/path</c>) resolve by that
|
||||||
|
/// same name. A stale <c>/ytLive;component/…</c> compiles, runs, and then fails <b>silently</b>
|
||||||
|
/// the moment that particular view opens — a blank window icon, a missing logo in the overlay
|
||||||
|
/// host. Nobody notices until a dialog is opened at the wrong time, so the rename is exactly the
|
||||||
|
/// kind of change that needs a source-level guard.</item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// <para>The C# namespaces stay <c>ytLive</c>: they are not user-visible, and the file churn
|
||||||
|
/// would buy nothing. Only the assembly name and the URIs that quote it move.</para>
|
||||||
|
/// </summary>
|
||||||
|
public class AssemblyIdentityTests
|
||||||
|
{
|
||||||
|
private const string ShipName = "LlamaCasty";
|
||||||
|
private const string LegacyName = "ytLive";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void The_assembly_is_LlamaCasty_and_no_pack_uri_still_names_the_legacy_assembly()
|
||||||
|
{
|
||||||
|
Assert.Equal(ShipName, typeof(MainWindow).Assembly.GetName().Name);
|
||||||
|
|
||||||
|
var root = RepoRoot();
|
||||||
|
var stale = Directory.EnumerateFiles(root, "*.xaml", SearchOption.AllDirectories)
|
||||||
|
.Where(path => !IsBuildOutput(path))
|
||||||
|
.Where(path => File.ReadAllText(path).Contains("/" + LegacyName + ";component/", StringComparison.Ordinal))
|
||||||
|
.Select(path => Path.GetRelativePath(root, path))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.True(stale.Length == 0,
|
||||||
|
"These XAML files still point at the legacy assembly name in a pack URI, which " +
|
||||||
|
"fails silently at runtime: " + string.Join(", ", stale));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Walks up from the test output directory to the repo root (the folder holding
|
||||||
|
/// <c>ytLive.csproj</c>).</summary>
|
||||||
|
private static string RepoRoot()
|
||||||
|
{
|
||||||
|
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (dir != null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(dir.FullName, "ytLive.csproj"))) return dir.FullName;
|
||||||
|
dir = dir.Parent;
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("Could not locate the repo root from " + AppContext.BaseDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Publish and compile output carry copies of the XAML (and old generated XAML);
|
||||||
|
/// only the source tree is the thing under test.</summary>
|
||||||
|
private static bool IsBuildOutput(string path)
|
||||||
|
{
|
||||||
|
var parts = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
return parts.Any(part => part.Equals("bin", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| part.Equals("obj", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| part.Equals("artifacts", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,7 +97,7 @@ public class StorePackagingTests
|
|||||||
var application = package.Element(Foundation + "Applications")!
|
var application = package.Element(Foundation + "Applications")!
|
||||||
.Element(Foundation + "Application")!;
|
.Element(Foundation + "Application")!;
|
||||||
Assert.Equal("Windows.FullTrustApplication", (string?)application.Attribute("EntryPoint"));
|
Assert.Equal("Windows.FullTrustApplication", (string?)application.Attribute("EntryPoint"));
|
||||||
Assert.Equal("ytLive.exe", (string?)application.Attribute("Executable"));
|
Assert.Equal("LlamaCasty.exe", (string?)application.Attribute("Executable"));
|
||||||
Assert.Equal("mediumIL", (string?)application.Attribute(Uap10 + "TrustLevel"));
|
Assert.Equal("mediumIL", (string?)application.Attribute(Uap10 + "TrustLevel"));
|
||||||
Assert.Equal("packagedClassicApp", (string?)application.Attribute(Uap10 + "RuntimeBehavior"));
|
Assert.Equal("packagedClassicApp", (string?)application.Attribute(Uap10 + "RuntimeBehavior"));
|
||||||
|
|
||||||
|
|||||||
+17
-2
@@ -8,7 +8,10 @@
|
|||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
<ApplicationIcon>Assets\llamacasty-icon.ico</ApplicationIcon>
|
<ApplicationIcon>Assets\llamacasty-icon.ico</ApplicationIcon>
|
||||||
<AssemblyName>ytLive</AssemblyName>
|
<!-- The shipped exe is LlamaCasty.exe (renamed 2026-11-01, before the first Steam depot so
|
||||||
|
the store never sees the legacy name). RootNamespace stays `ytLive` — the C# namespaces
|
||||||
|
are not user-visible and renaming them would churn every file for no benefit. -->
|
||||||
|
<AssemblyName>LlamaCasty</AssemblyName>
|
||||||
<RootNamespace>ytLive</RootNamespace>
|
<RootNamespace>ytLive</RootNamespace>
|
||||||
<!-- 64-bit Windows only (creator ruling 2026-10-09). The bundled ffmpeg is the win64 build
|
<!-- 64-bit Windows only (creator ruling 2026-10-09). The bundled ffmpeg is the win64 build
|
||||||
and ONNX Runtime ships no win-x86 native, so a 32-bit process could never stream; a
|
and ONNX Runtime ships no win-x86 native, so a 32-bit process could never stream; a
|
||||||
@@ -116,7 +119,19 @@
|
|||||||
from a pinned URL, verifying SHA-256 first. CopyToPublishDirectory=Always is the
|
from a pinned URL, verifying SHA-256 first. CopyToPublishDirectory=Always is the
|
||||||
one that matters for the MSIX package. -->
|
one that matters for the MSIX package. -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="ffmpeg\**" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Always" />
|
<!-- `Remove` first: the SDK's default None glob already matched ffmpeg\**, so an Include
|
||||||
|
without removing it produced DUPLICATE items — one carrying the copy metadata and one
|
||||||
|
with none. Harmless until PublishSingleFile + IncludeNativeLibrariesForSelfExtract came
|
||||||
|
along, where the bundler collapses the duplicate and the folder silently stops being
|
||||||
|
published (found 2026-11-01: a 9-file publish with no ffmpeg in it). -->
|
||||||
|
<!-- ⛔ ExcludeFromSingleFile is load-bearing. With PublishSingleFile +
|
||||||
|
IncludeNativeLibrariesForSelfExtract the bundler treats the libav*.dll family as native
|
||||||
|
libs and swallows them INTO the exe — a 182 MB exe with no ffmpeg\ folder beside it, so
|
||||||
|
the locator finds nothing and every stream fails at runtime. The metadata keeps the
|
||||||
|
folder a folder. (Found 2026-11-01 by diffing the publish output against a control
|
||||||
|
publish without the single-file flags.) -->
|
||||||
|
<None Remove="ffmpeg\**" />
|
||||||
|
<None Include="ffmpeg\**" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Always" ExcludeFromSingleFile="true" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="GuardBundledFfmpeg" BeforeTargets="CoreCompile">
|
<Target Name="GuardBundledFfmpeg" BeforeTargets="CoreCompile">
|
||||||
|
|||||||
Reference in New Issue
Block a user