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:
2026-11-01 07:01:50 -08:00
parent 8f79b20a18
commit abfab25685
15 changed files with 240 additions and 54 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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"
+29
View File
@@ -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 →
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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}">
+11 -15
View File
@@ -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
View File
@@ -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),
+1 -1
View File
@@ -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
+62
View File
@@ -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"
+71
View File
@@ -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));
}
}
+1 -1
View File
@@ -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
View File
@@ -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">