Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ jobs:
}
"rid=$rid" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
$ErrorActionPreference = 'Stop'
Write-Host "Packing WhatsBox pointer + WhatsBox.$rid into bin/ before WhatsDemo restore"
Write-Host "Packing Inbox + WhatsBox pointer + WhatsBox.$rid into bin/ before WhatsDemo restore"
# GeneratePackageOnBuild=true makes `dotnet pack` skip Build (NU5026: WhatsBox.dll missing).
dotnet pack src/Inbox/Inbox.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:inbox-pack.binlog
if ($LASTEXITCODE -ne 0) { throw "Inbox pack failed with exit code $LASTEXITCODE." }
dotnet pack src/WhatsBox/WhatsBox.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:pointer-pack.binlog
if ($LASTEXITCODE -ne 0) { throw "WhatsBox pointer pack failed with exit code $LASTEXITCODE." }
dotnet pack src/WhatsBox/WhatsBox.csproj -c $env:Configuration -r $rid -p:GeneratePackageOnBuild=false -bl:"pack-$rid.binlog"
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ jobs:
- name: 📦 pack
run: |
$ErrorActionPreference = 'Stop'
dotnet pack src/Inbox/Inbox.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:"inbox-pack.binlog"
if ($LASTEXITCODE -ne 0) { throw "Inbox pack failed with exit code $LASTEXITCODE." }
dotnet pack src/WhatsBox/WhatsBox.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:"pointer-pack.binlog"
if ($LASTEXITCODE -ne 0) { throw "WhatsBox pointer pack failed with exit code $LASTEXITCODE." }
dotnet pack src/WhatsBox/WhatsBox.csproj -c $env:Configuration -r ${{ matrix.rid }} -p:GeneratePackageOnBuild=false -bl:"pack-${{ matrix.rid }}.binlog"
Expand Down Expand Up @@ -115,7 +117,9 @@ jobs:
}
$rid = "$os-$cpu"
$ErrorActionPreference = 'Stop'
Write-Host "Packing WhatsBox pointer + WhatsBox.$rid into bin/ before WhatsDemo restore"
Write-Host "Packing Inbox + WhatsBox pointer + WhatsBox.$rid into bin/ before WhatsDemo restore"
dotnet pack src/Inbox/Inbox.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:inbox-pack.binlog
if ($LASTEXITCODE -ne 0) { throw "Inbox pack failed with exit code $LASTEXITCODE." }
dotnet pack src/WhatsBox/WhatsBox.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:pointer-pack.binlog
if ($LASTEXITCODE -ne 0) { throw "WhatsBox pointer pack failed with exit code $LASTEXITCODE." }
dotnet pack src/WhatsBox/WhatsBox.csproj -c $env:Configuration -r $rid -p:GeneratePackageOnBuild=false -bl:"pack-$rid.binlog"
Expand All @@ -130,6 +134,8 @@ jobs:
- name: 📦 pointer
run: |
$ErrorActionPreference = 'Stop'
dotnet pack src/Inbox/Inbox.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:inbox-pack.binlog
if ($LASTEXITCODE -ne 0) { throw "Inbox pack failed with exit code $LASTEXITCODE." }
dotnet pack src/WhatsBox/WhatsBox.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:pointer-pack.binlog
if ($LASTEXITCODE -ne 0) { throw "WhatsBox pointer pack failed with exit code $LASTEXITCODE." }
dotnet pack src/WhatsDemo/WhatsDemo.csproj -c $env:Configuration -p:GeneratePackageOnBuild=false -bl:pointer-pack-wd.binlog
Expand All @@ -140,6 +146,7 @@ jobs:
with:
name: package-pointer
path: |
bin/Inbox.*.nupkg
bin/WhatsBox.*.nupkg
bin/wd.*.nupkg
retention-days: 30
Expand Down
6 changes: 5 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ Target framework: `net10.0`. The managed surface is AOT-compatible (source-gener
You only reference `WhatsBox`. Restore and `dotnet publish -r <rid>` pull the
matching `WhatsBox.{rid}` package automatically. The sidecar lands next to the
app (`AppContext.BaseDirectory`); `WhatsBoxClient` starts it from there — never
from the current working directory.
from the current working directory. The pointer package depends on [`Inbox`](https://www.nuget.org/packages/Inbox)
(`InboxClient`); Inbox's RID packing targets are **not** transitive.

Adapters for other products PackageReference `Inbox` directly (or ProjectReference
it and import `Inbox.targets`) so pointer + `dotnet pack -r` packaging is shared.

```bash
dotnet add package WhatsBox
Expand Down
20 changes: 18 additions & 2 deletions src/Inbox/Inbox.csproj
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PackageId>Inbox</PackageId>
<RootNamespace>Inbox</RootNamespace>
<AssemblyName>Inbox</AssemblyName>
<Description>Managed reference client for an Inbox Protocol-implementation CLI (JSON-RPC 2.0, NDJSON over stdio).</Description>
<Description>Managed reference client for an Inbox Protocol-implementation CLI (JSON-RPC 2.0, NDJSON over stdio), plus RID packing targets for native adapters.</Description>
<IsAotCompatible>true</IsAotCompatible>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Tests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JsonPeek" Version="1.3.0" PrivateAssets="all" GeneratePathProperty="true" />
<PackageReference Include="JsonPoke" Version="1.3.0" PrivateAssets="all" GeneratePathProperty="true" />
</ItemGroup>
<ItemGroup>
<None Update="build\Inbox.targets" Pack="true" PackagePath="build\Inbox.targets" />
</ItemGroup>
<Target Name="PackInboxBuildTasks" BeforeTargets="_GetPackageFiles;GenerateNuspec">
<Error Condition="!Exists('$(PkgJsonPoke)\build\JsonPoke.dll')" Text="JsonPoke was not restored; cannot pack Inbox.targets task assemblies." />
<Error Condition="!Exists('$(PkgJsonPeek)\build\JsonPeek.dll')" Text="JsonPeek was not restored; cannot pack Inbox.targets task assemblies." />
<ItemGroup>
<None Include="$(PkgJsonPoke)\build\JsonPoke.dll" Pack="true" PackagePath="build\" Visible="false" />
<None Include="$(PkgJsonPoke)\build\Newtonsoft.Json.dll" Pack="true" PackagePath="build\" Visible="false" />
<None Include="$(PkgJsonPeek)\build\JsonPeek.dll" Pack="true" PackagePath="build\" Visible="false" />
</ItemGroup>
</Target>
</Project>
81 changes: 81 additions & 0 deletions src/Inbox/build/Inbox.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<Project>
<!--
Auto-imported from the Inbox nupkg via build/Inbox.targets (not buildTransitive).
Adapters that ProjectReference Inbox must Import this file themselves.
Pointer + RID native packing applies only when the adapter sets RuntimeIdentifiers.
Without that, this is just InboxClient — no runtime.json, no PackageId suffix.
-->
<PropertyGroup>
<InboxTargetsImported>true</InboxTargetsImported>
<InboxPackageId Condition="'$(InboxPackageId)' == ''">$(PackageId)</InboxPackageId>
</PropertyGroup>

<!-- Pointer + RID pack split is opt-in: the adapter must declare RuntimeIdentifiers. -->
<PropertyGroup Condition="'$(RuntimeIdentifiers)' != ''">
<NoWarn>$(NoWarn);NU5128</NoWarn>
<IncludeBuildOutput Condition="'$(RuntimeIdentifier)' == ''">true</IncludeBuildOutput>
<IncludeBuildOutput Condition="'$(RuntimeIdentifier)' != ''">false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking Condition="'$(RuntimeIdentifier)' != ''">true</SuppressDependenciesWhenPacking>
<PackageId Condition="'$(RuntimeIdentifier)' != ''">$(InboxPackageId).$(RuntimeIdentifier)</PackageId>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);PackInboxNativeBinary</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>

<!-- Nupkg layout: task assemblies sit next to this file. P2P imports rely on a JsonPoke/JsonPeek PackageReference in the adapter. -->
<UsingTask TaskName="JsonPoke" AssemblyFile="$(MSBuildThisFileDirectory)JsonPoke.dll" Condition="Exists('$(MSBuildThisFileDirectory)JsonPoke.dll')" />
<UsingTask TaskName="JsonPeek" AssemblyFile="$(MSBuildThisFileDirectory)JsonPeek.dll" Condition="Exists('$(MSBuildThisFileDirectory)JsonPeek.dll')" />

<Target Name="WriteInboxRuntimeJson"
BeforeTargets="_GetPackageFiles;GenerateNuspec"
Condition="'$(RuntimeIdentifiers)' != '' and '$(RuntimeIdentifier)' == ''">
<PropertyGroup>
<_InboxRuntimeJson>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)runtime.json'))</_InboxRuntimeJson>
</PropertyGroup>
<ItemGroup>
<_InboxRid Include="$(RuntimeIdentifiers)" />
</ItemGroup>
<Error Condition="'@(_InboxRid)' == ''" Text="Inbox pointer pack requires RuntimeIdentifiers so runtime.json can map RID packages." />
<MakeDir Directories="$([System.IO.Path]::GetDirectoryName($(_InboxRuntimeJson)))" />
<WriteLinesToFile File="$(_InboxRuntimeJson)" Lines="{&quot;runtimes&quot;:{}}" Overwrite="true" />
<JsonPoke
ContentPath="$(_InboxRuntimeJson)"
Query="$.runtimes['%(_InboxRid.Identity)'].$(InboxPackageId)['$(InboxPackageId).%(_InboxRid.Identity)']"
Value="'[$(Version), )'" />
<JsonPeek ContentPath="$(_InboxRuntimeJson)" Query="$.runtimes">
<Output TaskParameter="Result" ItemName="_InboxWrittenRuntimes" />
</JsonPeek>
<Error
Condition="'@(_InboxWrittenRuntimes)' == ''"
Text="runtime.json has no runtimes after writing RIDs from RuntimeIdentifiers." />
<ItemGroup>
<None Include="$(_InboxRuntimeJson)" Pack="true" PackagePath="runtime.json" />
</ItemGroup>
</Target>

<Target Name="IncludeInboxNative"
AfterTargets="$(InboxIncludeNativeAfterTargets)"
BeforeTargets="GetCopyToOutputDirectoryItems;AssignTargetPaths"
Condition="'$(RuntimeIdentifiers)' != '' and '$(RuntimeIdentifier)' == '' and '$(InboxNativeBinary)' != ''">
<PropertyGroup>
<_InboxNativeTargetPath Condition="'$(InboxNativeName)' != ''">$(InboxNativeName)</_InboxNativeTargetPath>
<_InboxNativeTargetPath Condition="'$(_InboxNativeTargetPath)' == ''">$([System.IO.Path]::GetFileName($(InboxNativeBinary)))</_InboxNativeTargetPath>
</PropertyGroup>
<ItemGroup>
<ContentWithTargetPath Include="$(InboxNativeBinary)" Condition="Exists('$(InboxNativeBinary)')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<TargetPath>$(_InboxNativeTargetPath)</TargetPath>
<Visible>false</Visible>
<Pack>false</Pack>
</ContentWithTargetPath>
</ItemGroup>
</Target>

<Target Name="PackInboxNativeBinary" DependsOnTargets="$(InboxPackNativeDependsOn)" Condition="'$(RuntimeIdentifiers)' != '' and '$(RuntimeIdentifier)' != ''" Returns="@(TfmSpecificPackageFile)">
<Error Condition="'$(InboxNativeBinary)' == '' or !Exists('$(InboxNativeBinary)')" Text="Inbox RID pack requires InboxNativeBinary so the RID package can expose runtimes/$(RuntimeIdentifier)/native/." />
<ItemGroup>
<TfmSpecificPackageFile Include="$(InboxNativeBinary)">
<PackagePath>runtimes/$(RuntimeIdentifier)/native/</PackagePath>
</TfmSpecificPackageFile>
</ItemGroup>
</Target>
</Project>
28 changes: 28 additions & 0 deletions src/Inbox/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
The [`Inbox`](https://www.nuget.org/packages/Inbox) package is the managed
**Inbox Protocol** client (`InboxClient`) plus non-transitive MSBuild targets
for adapters that ship a native sidecar.

```xml
<PackageReference Include="Inbox" Version="*" />
```

Apps that only want WhatsApp should PackageReference `WhatsBox` instead — these
targets are **not** transitive.

## Adapter packing

`Inbox.targets` is imported only by a **direct** reference (nupkg `build/`, not
`buildTransitive/`). In this repo, `WhatsBox` ProjectReferences Inbox and
imports the file by hand.

Pointer + RID packing is **opt-in**. Declare `RuntimeIdentifiers` on the adapter
(and set `InboxNativeBinary`). Without that property, Inbox packs as a plain
managed library — no `runtime.json`, no `PackageId` suffix, no native RID assets.

The adapter:

1. Sets `RuntimeIdentifiers` and builds its native binary (`InboxNativeBinary`,
optionally `InboxNativeName`, `InboxPackNativeDependsOn`,
`InboxIncludeNativeAfterTargets`).
2. Packs the pointer: `dotnet pack` → adapter DLL + `runtime.json`.
3. Packs each RID: `dotnet pack -r {rid}` → `runtimes/{rid}/native/` only.
46 changes: 38 additions & 8 deletions src/Tests/WhatsBoxPackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void ResolveBinaryPath_finds_project_reference_native_and_version_runs()
}

[Fact]
public void WriteWhatsBoxRuntimeJson_maps_six_rids_to_rid_packages()
public void WriteInboxRuntimeJson_maps_six_rids_to_rid_packages()
{
var repo = FindRepoRoot();
var project = Path.Combine(repo, "src", "WhatsBox", "WhatsBox.csproj");
Expand All @@ -61,7 +61,7 @@ public void WriteWhatsBoxRuntimeJson_maps_six_rids_to_rid_packages()
start.ArgumentList.Add("msbuild");
start.ArgumentList.Add(project);
start.ArgumentList.Add("-restore");
start.ArgumentList.Add("-t:WriteWhatsBoxRuntimeJson");
start.ArgumentList.Add("-t:WriteInboxRuntimeJson");
start.ArgumentList.Add("-p:Configuration=" + configuration);
start.ArgumentList.Add("-p:DesignTimeBuild=true");
start.ArgumentList.Add("-p:GeneratePackageOnBuild=false");
Expand Down Expand Up @@ -98,22 +98,46 @@ public void Pointer_and_rid_csproj_use_calc_pack_split()
var slnx = File.ReadAllText(Path.Combine(repo, "WhatsBox.slnx"));
Assert.Contains("src/Inbox/Inbox.csproj", slnx);
var inbox = File.ReadAllText(Path.Combine(repo, "src", "Inbox", "Inbox.csproj"));
Assert.Contains("<PackageId>Inbox</PackageId>", inbox);
Assert.Contains("<AssemblyName>Inbox</AssemblyName>", inbox);
Assert.Contains("<RootNamespace>Inbox</RootNamespace>", inbox);
Assert.Contains(@"PackagePath=""build\Inbox.targets""", inbox);
Assert.Contains(@"<None Update=""build\Inbox.targets""", inbox);
Assert.DoesNotContain("<Import ", inbox);
Assert.DoesNotContain("buildTransitive", inbox, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("<IsPackable>false</IsPackable>", inbox);
Assert.DoesNotContain("whatsbox.exe", inbox, StringComparison.OrdinalIgnoreCase);

var targets = File.ReadAllText(Path.Combine(repo, "src", "Inbox", "build", "Inbox.targets"));
Assert.Contains("<InboxTargetsImported>true</InboxTargetsImported>", targets);
Assert.Contains("buildTransitive", targets);
Assert.Contains("Condition=\"'$(RuntimeIdentifiers)' != ''\"", targets);
Assert.Contains("$(InboxPackageId).$(RuntimeIdentifier)", targets);
Assert.Contains("WriteInboxRuntimeJson", targets);
Assert.Contains("PackInboxNativeBinary", targets);
Assert.DoesNotContain("go build", targets, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("whatsbox.exe", targets, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("<RuntimeIdentifiers Condition=", targets);
Assert.DoesNotContain("win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64", targets);

var csproj = File.ReadAllText(Path.Combine(repo, "src", "WhatsBox", "WhatsBox.csproj"));
Assert.Contains("<PackageId>WhatsBox</PackageId>", csproj);
Assert.Contains(@"..\Inbox\Inbox.csproj", csproj);
Assert.Contains("WhatsBox.$(RuntimeIdentifier)", csproj);
Assert.Contains("<IncludeBuildOutput Condition=\"'$(RuntimeIdentifier)' == ''\">true</IncludeBuildOutput>", csproj);
Assert.Contains("<IncludeBuildOutput Condition=\"'$(RuntimeIdentifier)' != ''\">false</IncludeBuildOutput>", csproj);
Assert.Contains(@"..\Inbox\build\Inbox.targets", csproj);
Assert.Contains("<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>", csproj);
Assert.DoesNotContain("PrivateAssets=\"all\"", csproj.Substring(csproj.IndexOf(@"..\Inbox\Inbox.csproj", StringComparison.Ordinal)));
Assert.DoesNotContain("WhatsBox.$(RuntimeIdentifier)", csproj);
Assert.DoesNotContain("IncludeBuildOutput", csproj);
Assert.DoesNotContain("WriteWhatsBoxRuntimeJson", csproj);
Assert.DoesNotContain("PackWhatsBoxNativeBinary", csproj);
Assert.DoesNotContain("PackInboxReferenceOutput", csproj);
Assert.DoesNotContain("<PackAsTool", csproj);
Assert.Contains("InboxNativeBinary", csproj);
Assert.Contains("BuildWhatsBoxRidNative", csproj);
foreach (var rid in SupportedRids)
Assert.Contains(rid, csproj);

var template = File.ReadAllText(Path.Combine(repo, "src", "WhatsBox", "runtime.json"));
using var doc = JsonDocument.Parse(template);
Assert.True(doc.RootElement.TryGetProperty("runtimes", out _));
Assert.False(File.Exists(Path.Combine(repo, "src", "WhatsBox", "runtime.json")));
}

[Fact]
Expand Down Expand Up @@ -150,7 +174,10 @@ public void Workflows_have_os_matrix_rid_pack_and_pointer_collect()

var build = File.ReadAllText(Path.Combine(repo, ".github", "workflows", "build.yml"));
Assert.Contains("os-matrix.json", build);
Assert.Contains("dotnet pack src/Inbox/Inbox.csproj", build);
Assert.Contains("dotnet pack src/WhatsBox/WhatsBox.csproj", build);
Assert.True(build.IndexOf("dotnet pack src/Inbox/Inbox.csproj", StringComparison.Ordinal) <
build.IndexOf("dotnet pack src/WhatsBox/WhatsBox.csproj", StringComparison.Ordinal));
Assert.Contains("dotnet pack src/WhatsDemo/WhatsDemo.csproj", build);
Assert.Contains("name: package-${{ steps.rid.outputs.rid }}", build);

Expand All @@ -166,7 +193,10 @@ public void Workflows_have_os_matrix_rid_pack_and_pointer_collect()
Assert.Contains("macos-15-intel", publish);
Assert.Contains("name: package-${{ matrix.rid }}", publish);
Assert.Contains("pattern: package-*", publish);
Assert.Contains("dotnet pack src/Inbox/Inbox.csproj", publish);
Assert.Contains("dotnet pack src/WhatsBox/WhatsBox.csproj", publish);
Assert.True(publish.IndexOf("dotnet pack src/Inbox/Inbox.csproj", StringComparison.Ordinal) <
publish.IndexOf("dotnet pack src/WhatsBox/WhatsBox.csproj", StringComparison.Ordinal));
Assert.Contains("dotnet pack src/WhatsDemo/WhatsDemo.csproj", publish);
Assert.DoesNotContain("<PackAsTool", File.ReadAllText(Path.Combine(repo, "src", "WhatsBox", "WhatsBox.csproj")));
Assert.Matches(new Regex(@"pointerPackages"), publish);
Expand Down
Loading
Loading