Skip to content

Commit eb92e08

Browse files
Fetch Copilot CLI at build time
1 parent c7e0765 commit eb92e08

4 files changed

Lines changed: 146 additions & 32 deletions

File tree

dotnet/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
bin/
33
obj/
44

5+
# Generated build props (contains CLI version)
6+
src/build/GitHub.Copilot.SDK.props
7+
58
# NuGet packages
69
*.nupkg
710
*.snupkg

dotnet/src/Client.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,9 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
867867

868868
private static async Task<(Process Process, int? DetectedLocalhostTcpPort)> StartCliServerAsync(CopilotClientOptions options, ILogger logger, CancellationToken cancellationToken)
869869
{
870-
var cliPath = options.CliPath ?? "copilot";
870+
// Use explicit path or bundled CLI - no PATH fallback
871+
var cliPath = options.CliPath ?? GetBundledCliPath(out var searchedPath)
872+
?? throw new InvalidOperationException($"Copilot CLI not found at '{searchedPath}'. Ensure the SDK NuGet package was restored correctly or provide an explicit CliPath.");
871873
var args = new List<string>();
872874

873875
if (options.CliArgs != null)
@@ -970,6 +972,14 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
970972
return (cliProcess, detectedLocalhostTcpPort);
971973
}
972974

975+
private static string? GetBundledCliPath(out string searchedPath)
976+
{
977+
var binaryName = OperatingSystem.IsWindows() ? "copilot.exe" : "copilot";
978+
var rid = System.Runtime.InteropServices.RuntimeInformation.RuntimeIdentifier;
979+
searchedPath = Path.Combine(AppContext.BaseDirectory, "runtimes", rid, "native", binaryName);
980+
return File.Exists(searchedPath) ? searchedPath : null;
981+
}
982+
973983
private static (string FileName, IEnumerable<string> Args) ResolveCliCommand(string cliPath, IEnumerable<string> args)
974984
{
975985
var isJsFile = cliPath.EndsWith(".js", StringComparison.OrdinalIgnoreCase);
@@ -979,13 +989,6 @@ private static (string FileName, IEnumerable<string> Args) ResolveCliCommand(str
979989
return ("node", new[] { cliPath }.Concat(args));
980990
}
981991

982-
// On Windows with UseShellExecute=false, Process.Start doesn't search PATHEXT,
983-
// so use cmd /c to let the shell resolve the executable
984-
if (OperatingSystem.IsWindows() && !Path.IsPathRooted(cliPath))
985-
{
986-
return ("cmd", new[] { "/c", cliPath }.Concat(args));
987-
}
988-
989992
return (cliPath, args);
990993
}
991994

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,56 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
8+
<Version>0.1.0</Version>
9+
<Description>SDK for programmatic control of GitHub Copilot CLI</Description>
10+
<Authors>GitHub</Authors>
11+
<Company>GitHub</Company>
12+
<Copyright>Copyright (c) Microsoft Corporation. All rights reserved.</Copyright>
13+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
14+
<PackageReadmeFile>README.md</PackageReadmeFile>
15+
<RepositoryUrl>https://github.com/github/copilot-sdk</RepositoryUrl>
16+
<PackageTags>github;copilot;sdk;jsonrpc;agent</PackageTags>
17+
<IsAotCompatible>true</IsAotCompatible>
18+
</PropertyGroup>
19+
20+
<ItemGroup>
21+
<None Include="../README.md" Pack="true" PackagePath="/" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="10.2.0" />
26+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
27+
<PackageReference Include="StreamJsonRpc" Version="2.24.84" PrivateAssets="compile" />
28+
<PackageReference Include="System.Text.Json" Version="10.0.2" />
29+
</ItemGroup>
30+
31+
<!-- Generate version props file at build time (gitignored) -->
32+
<Target Name="_GenerateVersionProps" BeforeTargets="BeforeBuild">
33+
<Exec Command="node -e &quot;console.log(require('./nodejs/package-lock.json').packages['node_modules/@github/copilot'].version)&quot;" WorkingDirectory="$(MSBuildThisFileDirectory)../.." ConsoleToMSBuild="true" StandardOutputImportance="low">
34+
<Output TaskParameter="ConsoleOutput" PropertyName="CopilotCliVersion" />
35+
</Exec>
36+
<Error Condition="'$(CopilotCliVersion)' == ''" Text="CopilotCliVersion could not be read from nodejs/package-lock.json" />
37+
<PropertyGroup>
38+
<_VersionPropsContent>
39+
<![CDATA[<Project>
340
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
5-
<ImplicitUsings>enable</ImplicitUsings>
6-
<Nullable>enable</Nullable>
7-
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
8-
<Version>0.1.0</Version>
9-
<Description>SDK for programmatic control of GitHub Copilot CLI</Description>
10-
<Authors>GitHub</Authors>
11-
<Company>GitHub</Company>
12-
<Copyright>Copyright (c) Microsoft Corporation. All rights reserved.</Copyright>
13-
<PackageLicenseExpression>MIT</PackageLicenseExpression>
14-
<PackageReadmeFile>README.md</PackageReadmeFile>
15-
<RepositoryUrl>https://github.com/github/copilot-sdk</RepositoryUrl>
16-
<PackageTags>github;copilot;sdk;jsonrpc;agent</PackageTags>
17-
<IsAotCompatible>true</IsAotCompatible>
41+
<CopilotCliVersion>$(CopilotCliVersion)</CopilotCliVersion>
1842
</PropertyGroup>
43+
</Project>]]>
44+
</_VersionPropsContent>
45+
</PropertyGroup>
46+
<WriteLinesToFile File="$(MSBuildThisFileDirectory)build\GitHub.Copilot.SDK.props" Lines="$(_VersionPropsContent)" Overwrite="true" WriteOnlyWhenDifferent="true" />
47+
</Target>
1948

20-
<ItemGroup>
21-
<None Include="../README.md" Pack="true" PackagePath="/" />
22-
</ItemGroup>
23-
24-
<ItemGroup>
25-
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="10.2.0" />
26-
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
27-
<PackageReference Include="StreamJsonRpc" Version="2.24.84" PrivateAssets="compile" />
28-
<PackageReference Include="System.Text.Json" Version="10.0.2" />
29-
</ItemGroup>
49+
<!-- Include .targets and .props files in package -->
50+
<!-- Also import the .targets for local dev (same logic consumers get) -->
51+
<ItemGroup>
52+
<None Include="build\GitHub.Copilot.SDK.*" Pack="true" PackagePath="build\" CopyToOutputDirectory="Never" />
53+
</ItemGroup>
54+
<Import Project="build\GitHub.Copilot.SDK.targets" />
3055

3156
</Project>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<Project>
2+
<!-- These targets run in consuming projects when they build -->
3+
<!-- CopilotCliVersion is imported from GitHub.Copilot.SDK.props (generated at SDK build time, packaged alongside) -->
4+
<Import Project="$(MSBuildThisFileDirectory)GitHub.Copilot.SDK.props" Condition="'$(CopilotCliVersion)' == '' And Exists('$(MSBuildThisFileDirectory)GitHub.Copilot.SDK.props')" />
5+
6+
<!-- Resolve RID: use explicit RuntimeIdentifier, or infer from current machine -->
7+
<PropertyGroup>
8+
<_CopilotRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</_CopilotRid>
9+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('Windows')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">win-x64</_CopilotRid>
10+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('Windows')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">win-arm64</_CopilotRid>
11+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('Linux')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">linux-x64</_CopilotRid>
12+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('Linux')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">linux-arm64</_CopilotRid>
13+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('OSX')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">osx-x64</_CopilotRid>
14+
<_CopilotRid Condition="'$(_CopilotRid)' == '' And $([MSBuild]::IsOSPlatform('OSX')) And '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">osx-arm64</_CopilotRid>
15+
</PropertyGroup>
16+
17+
<!-- Map RID to platform name used in npm/GitHub releases -->
18+
<PropertyGroup>
19+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'win-x64'">win32-x64</_CopilotPlatform>
20+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'win-arm64'">win32-arm64</_CopilotPlatform>
21+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'linux-x64'">linux-x64</_CopilotPlatform>
22+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'linux-arm64'">linux-arm64</_CopilotPlatform>
23+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'osx-x64'">darwin-x64</_CopilotPlatform>
24+
<_CopilotPlatform Condition="'$(_CopilotRid)' == 'osx-arm64'">darwin-arm64</_CopilotPlatform>
25+
<_CopilotBinary Condition="$(_CopilotRid.StartsWith('win-'))">copilot.exe</_CopilotBinary>
26+
<_CopilotBinary Condition="'$(_CopilotBinary)' == ''">copilot</_CopilotBinary>
27+
<_CopilotIsWindows Condition="$(_CopilotRid.StartsWith('win-'))">true</_CopilotIsWindows>
28+
</PropertyGroup>
29+
30+
<!-- Download and extract CLI binary -->
31+
<Target Name="_DownloadCopilotCli" BeforeTargets="BeforeBuild" Condition="'$(_CopilotPlatform)' != ''">
32+
<Error Condition="'$(CopilotCliVersion)' == ''" Text="CopilotCliVersion is not set. The GitHub.Copilot.SDK.Version.props file may be missing from the NuGet package." />
33+
34+
<!-- Compute paths using version (now available) -->
35+
<PropertyGroup>
36+
<_CopilotCacheDir>$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform)\</_CopilotCacheDir>
37+
<_CopilotCliBinaryPath>$(_CopilotCacheDir)$(_CopilotBinary)</_CopilotCliBinaryPath>
38+
<_CopilotArchiveExt Condition="'$(_CopilotIsWindows)' == 'true'">.zip</_CopilotArchiveExt>
39+
<_CopilotArchiveExt Condition="'$(_CopilotIsWindows)' != 'true'">.tgz</_CopilotArchiveExt>
40+
<_CopilotArchivePath>$(_CopilotCacheDir)copilot$(_CopilotArchiveExt)</_CopilotArchivePath>
41+
<_CopilotDownloadUrl Condition="'$(_CopilotIsWindows)' == 'true'">https://github.com/github/copilot-cli/releases/download/v$(CopilotCliVersion)/copilot-$(_CopilotPlatform).zip</_CopilotDownloadUrl>
42+
<_CopilotDownloadUrl Condition="'$(_CopilotIsWindows)' != 'true'">https://registry.npmjs.org/@github/copilot-$(_CopilotPlatform)/-/copilot-$(_CopilotPlatform)-$(CopilotCliVersion).tgz</_CopilotDownloadUrl>
43+
</PropertyGroup>
44+
45+
<!-- Download if not cached -->
46+
<MakeDir Directories="$(_CopilotCacheDir)" Condition="!Exists('$(_CopilotCliBinaryPath)')" />
47+
<Message Importance="high" Text="Downloading Copilot CLI $(CopilotCliVersion) for $(_CopilotPlatform)..." Condition="!Exists('$(_CopilotCliBinaryPath)')" />
48+
<DownloadFile SourceUrl="$(_CopilotDownloadUrl)" DestinationFolder="$(_CopilotCacheDir)" DestinationFileName="copilot$(_CopilotArchiveExt)"
49+
Condition="!Exists('$(_CopilotCliBinaryPath)') And !Exists('$(_CopilotArchivePath)')" />
50+
51+
<!-- Extract: Windows uses Unzip, others use tar -->
52+
<Unzip SourceFiles="$(_CopilotArchivePath)" DestinationFolder="$(_CopilotCacheDir)"
53+
Condition="'$(_CopilotIsWindows)' == 'true' And !Exists('$(_CopilotCliBinaryPath)')" />
54+
<Exec Command="tar -xzf &quot;$(_CopilotArchivePath)&quot; --strip-components=1 -C &quot;$(_CopilotCacheDir)&quot;"
55+
Condition="'$(_CopilotIsWindows)' != 'true' And !Exists('$(_CopilotCliBinaryPath)')" />
56+
57+
<Error Condition="!Exists('$(_CopilotCliBinaryPath)')" Text="Failed to extract Copilot CLI binary to $(_CopilotCliBinaryPath)" />
58+
</Target>
59+
60+
<!-- Copy CLI binary to output runtimes folder and register for transitive copy -->
61+
<Target Name="_CopyCopilotCliToOutput" AfterTargets="Build" DependsOnTargets="_DownloadCopilotCli" Condition="'$(_CopilotPlatform)' != ''">
62+
<PropertyGroup>
63+
<_CopilotCacheDir>$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform)\</_CopilotCacheDir>
64+
<_CopilotCliBinaryPath>$(_CopilotCacheDir)$(_CopilotBinary)</_CopilotCliBinaryPath>
65+
<_CopilotOutputDir>$(OutDir)runtimes\$(_CopilotRid)\native\</_CopilotOutputDir>
66+
</PropertyGroup>
67+
<MakeDir Directories="$(_CopilotOutputDir)" />
68+
<Copy SourceFiles="$(_CopilotCliBinaryPath)" DestinationFolder="$(_CopilotOutputDir)" SkipUnchangedFiles="true" />
69+
</Target>
70+
71+
<!-- Register CLI binary as content so it flows through project references -->
72+
<Target Name="_RegisterCopilotCliForCopy" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="_DownloadCopilotCli" Condition="'$(_CopilotPlatform)' != ''">
73+
<PropertyGroup>
74+
<_CopilotCacheDir>$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform)\</_CopilotCacheDir>
75+
<_CopilotCliBinaryPath>$(_CopilotCacheDir)$(_CopilotBinary)</_CopilotCliBinaryPath>
76+
</PropertyGroup>
77+
<ItemGroup>
78+
<ContentWithTargetPath Include="$(_CopilotCliBinaryPath)"
79+
TargetPath="runtimes\$(_CopilotRid)\native\$(_CopilotBinary)"
80+
CopyToOutputDirectory="PreserveNewest" />
81+
</ItemGroup>
82+
</Target>
83+
</Project>

0 commit comments

Comments
 (0)