Skip to content

Commit 7404f37

Browse files
committed
Stage auxiliary runtime assets from npm packages
Preserve unknown package assets by default while filtering known CLI-only content in each SDK's existing staging path. Keep wrapper companions adjacent, migrate caches safely, and retain executable metadata for external runtime tools. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f5c3c30-0cf7-4b9a-9a3b-2b01ca073015
1 parent 0e64a4e commit 7404f37

19 files changed

Lines changed: 1317 additions & 247 deletions

File tree

‎dotnet/src/build/GitHub.Copilot.SDK.targets‎

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,39 @@
137137
shared-library name next to the CLI binary. -->
138138
<_CopilotRuntimeNodePath>$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\runtime.node</_CopilotRuntimeNodePath>
139139
<_CopilotRuntimeWrapperPath Condition="'$(_CopilotRuntimeWrapperPath)' == ''">$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\$(_CopilotRuntimeWrapper)</_CopilotRuntimeWrapperPath>
140+
<_CopilotRuntimeAssetManifest>$(_CopilotOutputDir)\.copilot-runtime-assets</_CopilotRuntimeAssetManifest>
140141
</PropertyGroup>
141142
<Error Condition="!Exists('$(_CopilotCliBinaryPath)')" Text="Copilot CLI binary not found at '$(_CopilotCliBinaryPath)'. Set CopilotCliBinaryPath to an existing CLI binary for the current RID, or remove CopilotSkipCliDownload=true to let the SDK download it." />
143+
<Error Condition="Exists('$(_CopilotRuntimeWrapperPath)') != Exists('$(_CopilotRuntimeNodePath)')" Text="Incomplete Copilot runtime bundle in '$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)': $(_CopilotRuntimeWrapper) and runtime.node are both required." />
144+
<ReadLinesFromFile File="$(_CopilotRuntimeAssetManifest)" Condition="Exists('$(_CopilotRuntimeAssetManifest)')">
145+
<Output TaskParameter="Lines" ItemName="_CopilotPreviousRuntimeAsset" />
146+
</ReadLinesFromFile>
147+
<ItemGroup>
148+
<_CopilotSafePreviousRuntimeAsset Include="@(_CopilotPreviousRuntimeAsset)"
149+
Condition="!$([System.IO.Path]::IsPathRooted('%(Identity)')) And !$([System.String]::Copy('%(Identity)').Contains('..'))" />
150+
</ItemGroup>
151+
<Delete Files="@(_CopilotSafePreviousRuntimeAsset->'$(_CopilotOutputDir)\%(Identity)')" />
152+
<Delete Files="$(_CopilotRuntimeAssetManifest)" />
153+
<ItemGroup Condition="Exists('$(_CopilotRuntimeWrapperPath)') And Exists('$(_CopilotRuntimeNodePath)')">
154+
<_CopilotRuntimeRootAsset Include="$(_CopilotCacheDir)\**\*"
155+
Exclude="$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot-sdk\**\*;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\preloads\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\sdk\**\*;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" />
156+
<_CopilotRuntimePrebuildAsset Include="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\**\*"
157+
Exclude="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\cli-native.node;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\mediaremote-adapter\**\*;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\copilot-runtime-bin*" />
158+
</ItemGroup>
142159
<MakeDir Directories="$(_CopilotOutputDir)" />
143160
<Copy SourceFiles="$(_CopilotCliBinaryPath)" DestinationFiles="$(_CopilotOutputDir)\$(_CopilotBinary)" SkipUnchangedFiles="true" />
144161
<Copy SourceFiles="$(_CopilotRuntimeNodePath)" DestinationFiles="$(_CopilotOutputDir)\$(_CopilotRuntimeLib)" SkipUnchangedFiles="true" Condition="Exists('$(_CopilotRuntimeNodePath)')" />
145-
<Delete Files="$(_CopilotOutputDir)\runtime.node;$(_CopilotOutputDir)\$(_CopilotRuntimeWrapper)"
146-
Condition="!Exists('$(_CopilotRuntimeWrapperPath)') Or !Exists('$(_CopilotRuntimeNodePath)')" />
147-
<Copy SourceFiles="$(_CopilotRuntimeNodePath)" DestinationFiles="$(_CopilotOutputDir)\runtime.node" SkipUnchangedFiles="true" Condition="Exists('$(_CopilotRuntimeWrapperPath)') And Exists('$(_CopilotRuntimeNodePath)')" />
148-
<Copy SourceFiles="$(_CopilotRuntimeWrapperPath)" DestinationFiles="$(_CopilotOutputDir)\$(_CopilotRuntimeWrapper)" SkipUnchangedFiles="true" Condition="Exists('$(_CopilotRuntimeWrapperPath)') And Exists('$(_CopilotRuntimeNodePath)')" />
162+
<Copy SourceFiles="@(_CopilotRuntimeRootAsset)"
163+
DestinationFiles="@(_CopilotRuntimeRootAsset->'$(_CopilotOutputDir)\%(RecursiveDir)%(Filename)%(Extension)')"
164+
SkipUnchangedFiles="true" />
165+
<Copy SourceFiles="@(_CopilotRuntimePrebuildAsset)"
166+
DestinationFiles="@(_CopilotRuntimePrebuildAsset->'$(_CopilotOutputDir)\%(RecursiveDir)%(Filename)%(Extension)')"
167+
SkipUnchangedFiles="true" />
168+
<WriteLinesToFile File="$(_CopilotRuntimeAssetManifest)"
169+
Lines="@(_CopilotRuntimeRootAsset->'%(RecursiveDir)%(Filename)%(Extension)');@(_CopilotRuntimePrebuildAsset->'%(RecursiveDir)%(Filename)%(Extension)')"
170+
Overwrite="true"
171+
Condition="'@(_CopilotRuntimeRootAsset)' != '' Or '@(_CopilotRuntimePrebuildAsset)' != ''" />
172+
<Delete Files="$(_CopilotOutputDir)\$(_CopilotRuntimeLib)" Condition="!Exists('$(_CopilotRuntimeNodePath)')" />
149173
</Target>
150174

151175
<!-- Register CLI binary as content so it flows through project references.
@@ -158,7 +182,14 @@
158182
<_CopilotRuntimeWrapperPath Condition="'$(_CopilotRuntimeWrapperPath)' == ''">$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\$(_CopilotRuntimeWrapper)</_CopilotRuntimeWrapperPath>
159183
</PropertyGroup>
160184
<Error Condition="!Exists('$(_CopilotCliBinaryPath)')" Text="Copilot CLI binary not found at '$(_CopilotCliBinaryPath)'. Set CopilotCliBinaryPath to an existing CLI binary for the current RID, or remove CopilotSkipCliDownload=true to let the SDK download it." />
185+
<Error Condition="Exists('$(_CopilotRuntimeWrapperPath)') != Exists('$(_CopilotRuntimeNodePath)')" Text="Incomplete Copilot runtime bundle in '$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)': $(_CopilotRuntimeWrapper) and runtime.node are both required." />
161186
<ItemGroup>
187+
<_CopilotRuntimeRootAsset Include="$(_CopilotCacheDir)\**\*"
188+
Exclude="$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot-sdk\**\*;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\preloads\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\sdk\**\*;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*"
189+
Condition="Exists('$(_CopilotRuntimeWrapperPath)') And Exists('$(_CopilotRuntimeNodePath)')" />
190+
<_CopilotRuntimePrebuildAsset Include="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\**\*"
191+
Exclude="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\cli-native.node;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\mediaremote-adapter\**\*;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\copilot-runtime-bin*"
192+
Condition="Exists('$(_CopilotRuntimeWrapperPath)') And Exists('$(_CopilotRuntimeNodePath)')" />
162193
<ContentWithTargetPath Include="$(_CopilotCliBinaryPath)"
163194
TargetPath="runtimes\$(_CopilotRid)\native\$(_CopilotBinary)"
164195
CopyToOutputDirectory="PreserveNewest" />
@@ -167,14 +198,12 @@
167198
TargetPath="runtimes\$(_CopilotRid)\native\$(_CopilotRuntimeLib)"
168199
CopyToOutputDirectory="PreserveNewest"
169200
Condition="Exists('$(_CopilotRuntimeNodePath)')" />
170-
<ContentWithTargetPath Include="$(_CopilotRuntimeNodePath)"
171-
TargetPath="runtimes\$(_CopilotRid)\native\runtime.node"
172-
CopyToOutputDirectory="PreserveNewest"
173-
Condition="Exists('$(_CopilotRuntimeWrapperPath)') And Exists('$(_CopilotRuntimeNodePath)')" />
174-
<ContentWithTargetPath Include="$(_CopilotRuntimeWrapperPath)"
175-
TargetPath="runtimes\$(_CopilotRid)\native\$(_CopilotRuntimeWrapper)"
176-
CopyToOutputDirectory="PreserveNewest"
177-
Condition="Exists('$(_CopilotRuntimeWrapperPath)') And Exists('$(_CopilotRuntimeNodePath)')" />
201+
<ContentWithTargetPath Include="@(_CopilotRuntimeRootAsset)"
202+
TargetPath="runtimes\$(_CopilotRid)\native\%(RecursiveDir)%(Filename)%(Extension)"
203+
CopyToOutputDirectory="PreserveNewest" />
204+
<ContentWithTargetPath Include="@(_CopilotRuntimePrebuildAsset)"
205+
TargetPath="runtimes\$(_CopilotRid)\native\%(RecursiveDir)%(Filename)%(Extension)"
206+
CopyToOutputDirectory="PreserveNewest" />
178207
</ItemGroup>
179208
</Target>
180209
</Project>

‎dotnet/test/E2E/BuiltinToolsE2ETests.cs‎

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -112,21 +112,9 @@ public async Task Should_Create_A_New_File()
112112
Assert.Contains("Created by test", msg?.Data.Content ?? string.Empty);
113113
}
114114

115-
// TODO(cli-1.0.81-2): the grep and glob built-in tools shell out to the CLI's
116-
// bundled ripgrep, which the runtime cannot locate when it is loaded in-process
117-
// over FFI ("Failed to execute ripgrep: No such file or directory"). The tool
118-
// then returns an error the recorded snapshots do not cover. Re-enable once the
119-
// in-process runtime resolves its bundled binaries.
120-
private static bool RipgrepUnavailable => E2ETestContext.UsesInProcessTransport;
121-
122115
[Fact]
123116
public async Task Should_Search_For_Patterns_In_Files()
124117
{
125-
if (RipgrepUnavailable)
126-
{
127-
return;
128-
}
129-
130118
await File.WriteAllTextAsync(Path.Join(Ctx.WorkDir, "data.txt"), "apple\nbanana\napricot\ncherry\n");
131119
var session = await CreateSessionAsync();
132120
var msg = await session.SendAndWaitAsync(new MessageOptions
@@ -141,11 +129,6 @@ public async Task Should_Search_For_Patterns_In_Files()
141129
[Fact]
142130
public async Task Should_Find_Files_By_Pattern()
143131
{
144-
if (RipgrepUnavailable)
145-
{
146-
return;
147-
}
148-
149132
Directory.CreateDirectory(Path.Join(Ctx.WorkDir, "src"));
150133
await File.WriteAllTextAsync(Path.Join(Ctx.WorkDir, "src", "index.ts"), "export const index = 1;");
151134
await File.WriteAllTextAsync(Path.Join(Ctx.WorkDir, "README.md"), "# Readme");

‎dotnet/test/Unit/MSBuildTargetsTests.cs‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,31 @@ public async Task PreinstalledCliBinaryPath_WithSkipCliDownload_StillCopiesToOut
105105
Assert.True(File.Exists(sandbox.ExpectedOutputBinary()), result.FailureMessage());
106106
}
107107

108+
[Fact]
109+
public async Task RuntimePackageAssets_AreFilteredAndCopiedToOutput()
110+
{
111+
using var sandbox = MSBuildSandbox.Create();
112+
var preinstalled = sandbox.WritePreinstalledBinary("fake-cli-contents");
113+
sandbox.WriteRuntimeCacheAsset("prebuilds", GetNpmPlatform(), "runtime.node", "runtime");
114+
sandbox.WriteRuntimeCacheAsset("prebuilds", GetNpmPlatform(),
115+
OperatingSystem.IsWindows() ? "copilot-runtime.exe" : "copilot-runtime", "wrapper");
116+
sandbox.WriteRuntimeCacheAsset("ripgrep", "bin", GetNpmPlatform(), "rg", "ripgrep");
117+
sandbox.WriteRuntimeCacheAsset("definitions", "future.json", "{}");
118+
sandbox.WriteRuntimeCacheAsset("app.js", "excluded");
119+
sandbox.WriteStaleOutputRuntimeAsset("obsolete", "tool", "stale");
120+
121+
var result = await sandbox.BuildAsync(new Dictionary<string, string>
122+
{
123+
["CopilotCliBinaryPath"] = preinstalled,
124+
});
125+
126+
Assert.True(result.Succeeded, result.FailureMessage());
127+
Assert.Equal("ripgrep", File.ReadAllText(sandbox.ExpectedRuntimeAsset("ripgrep", "bin", GetNpmPlatform(), "rg")));
128+
Assert.Equal("{}", File.ReadAllText(sandbox.ExpectedRuntimeAsset("definitions", "future.json")));
129+
Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("app.js")));
130+
Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("obsolete", "tool")));
131+
}
132+
108133
[Fact]
109134
public async Task PreinstalledCliBinaryPath_NonExistentFile_FailsWithActionableError()
110135
{
@@ -150,6 +175,17 @@ private static string FindTargetsFile([CallerFilePath] string? thisFile = null)
150175
"Could not locate GitHub.Copilot.SDK.targets relative to test assembly or source file.");
151176
}
152177

178+
private static string GetNpmPlatform()
179+
{
180+
var arch = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture
181+
== System.Runtime.InteropServices.Architecture.Arm64
182+
? "arm64"
183+
: "x64";
184+
if (OperatingSystem.IsWindows()) return $"win32-{arch}";
185+
if (OperatingSystem.IsMacOS()) return $"darwin-{arch}";
186+
return $"linux-{arch}";
187+
}
188+
153189
/// <summary>
154190
/// A throwaway directory containing a minimal csproj that imports the SDK targets
155191
/// file. Disposing removes the directory tree.
@@ -203,6 +239,40 @@ public string ExpectedOutputBinary()
203239
return Path.Combine(ProjectDir, "bin", "Debug", "net8.0", "runtimes", rid, "native", BinaryName);
204240
}
205241

242+
public void WriteRuntimeCacheAsset(params string[] pathAndContents)
243+
{
244+
var pathParts = pathAndContents[..^1];
245+
var path = Path.Combine(ProjectDir, "obj", "Debug", "net8.0", "copilot-cli", "0.0.0-test",
246+
GetNpmPlatform());
247+
foreach (var part in pathParts)
248+
{
249+
path = Path.Combine(path, part);
250+
}
251+
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
252+
File.WriteAllText(path, pathAndContents[^1]);
253+
}
254+
255+
public string ExpectedRuntimeAsset(params string[] pathParts)
256+
{
257+
var path = Path.Combine(ProjectDir, "bin", "Debug", "net8.0", "runtimes", GetPortableRid(), "native");
258+
foreach (var part in pathParts)
259+
{
260+
path = Path.Combine(path, part);
261+
}
262+
return path;
263+
}
264+
265+
public void WriteStaleOutputRuntimeAsset(params string[] pathAndContents)
266+
{
267+
var relativeParts = pathAndContents[..^1];
268+
var path = ExpectedRuntimeAsset(relativeParts);
269+
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
270+
File.WriteAllText(path, pathAndContents[^1]);
271+
var manifest = ExpectedRuntimeAsset(".copilot-runtime-assets");
272+
Directory.CreateDirectory(Path.GetDirectoryName(manifest)!);
273+
File.WriteAllText(manifest, string.Join(Path.DirectorySeparatorChar, relativeParts) + Environment.NewLine);
274+
}
275+
206276
public async Task<BuildResult> BuildAsync(IDictionary<string, string> properties)
207277
{
208278
var args = new StringBuilder("build --nologo -clp:NoSummary");

0 commit comments

Comments
 (0)