Skip to content

Commit 00bc009

Browse files
CopilotedburnsCopilot
committed
[Java] Add copilot-native module producing linux-x64 classifier JAR for runtime.node (#2236)
* Initial plan * Add copilot-native module with linux-x64 classifier JAR Co-authored-by: edburns <75821+edburns@users.noreply.github.com> * Address Copilot review: harden cache stamp and add platform.properties assertion - Persist integrity hash and binary digest in the .version stamp file so corrupted binaries or lockfile integrity changes are detected on cache hit. - Add zipentry assertion for platform.properties in the classifier JAR structural guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: edburns <75821+edburns@users.noreply.github.com> Co-authored-by: Ed Burns <edburns@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 29798c4 commit 00bc009

5 files changed

Lines changed: 329 additions & 3 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ docs/.validation/
1616
# Java
1717
java/target
1818
java/sdk/target
19+
java/copilot-native/target
1920
java/smoke-test
2021
java/.classpath
2122
java/.project

java/copilot-native/pom.xml

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright (c) Microsoft Corporation. All rights reserved.
4+
-->
5+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
6+
<modelVersion>4.0.0</modelVersion>
7+
8+
<parent>
9+
<groupId>com.github</groupId>
10+
<artifactId>copilot-sdk-java-parent</artifactId>
11+
<version>1.0.10-preview.0-SNAPSHOT</version>
12+
<relativePath>../pom.xml</relativePath>
13+
</parent>
14+
15+
<groupId>com.github</groupId>
16+
<artifactId>copilot-sdk-java-runtime</artifactId>
17+
<packaging>jar</packaging>
18+
19+
<name>GitHub Copilot SDK :: Java :: Native Runtime</name>
20+
<description>Native runtime binaries for the GitHub Copilot Java SDK, published as per-platform classifier JARs</description>
21+
22+
<properties>
23+
<!--
24+
Root of the monorepo. This module lives at java/copilot-native/, so
25+
two levels up reaches the monorepo root. Used to locate
26+
nodejs/package-lock.json, which pins both the runtime version and
27+
its SHA-512 integrity hash.
28+
-->
29+
<copilot.sdk.root>${project.basedir}/../..</copilot.sdk.root>
30+
<!--
31+
Phase 4 hard invariant: linux-x64 is the only supported platform.
32+
Additional classifiers are added in a later phase, each with its own
33+
fetch execution and maven-jar-plugin execution.
34+
-->
35+
<copilot.native.classifier>linux-x64</copilot.native.classifier>
36+
<copilot.native.staging>${project.build.directory}/native-staging</copilot.native.staging>
37+
<!--
38+
The native runtime is downloaded from npm; this module is published
39+
to Maven Central alongside the sdk module.
40+
-->
41+
<maven.deploy.skip>false</maven.deploy.skip>
42+
</properties>
43+
44+
<build>
45+
<resources>
46+
<!--
47+
Placeholder resource for the primary JAR. Filtered so that
48+
${project.version} is substituted. Deliberately contains no
49+
native binaries: the primary JAR exists only to satisfy Maven
50+
Central validation, which requires a main artifact.
51+
-->
52+
<resource>
53+
<directory>src/main/resources</directory>
54+
<filtering>true</filtering>
55+
</resource>
56+
</resources>
57+
<plugins>
58+
<!--
59+
Download and verify the native runtime binary. The helper script
60+
reads the pinned version and SHA-512 integrity hash for
61+
@github/copilot-<classifier> from nodejs/package-lock.json,
62+
runs `npm pack`, verifies the tarball against that hash, and
63+
extracts runtime.node into the staging directory. Requires
64+
Node.js and npm — already required for the Java E2E tests.
65+
-->
66+
<plugin>
67+
<groupId>org.codehaus.mojo</groupId>
68+
<artifactId>exec-maven-plugin</artifactId>
69+
<executions>
70+
<execution>
71+
<id>fetch-native-linux-x64</id>
72+
<phase>generate-resources</phase>
73+
<goals>
74+
<goal>exec</goal>
75+
</goals>
76+
<configuration>
77+
<executable>node</executable>
78+
<arguments>
79+
<argument>${project.basedir}/scripts/fetch-native.mjs</argument>
80+
<argument>${copilot.sdk.root}</argument>
81+
<argument>${copilot.native.staging}</argument>
82+
<argument>${copilot.native.classifier}</argument>
83+
</arguments>
84+
</configuration>
85+
</execution>
86+
</executions>
87+
</plugin>
88+
<plugin>
89+
<groupId>org.apache.maven.plugins</groupId>
90+
<artifactId>maven-jar-plugin</artifactId>
91+
<executions>
92+
<!--
93+
Additional classifier JAR containing
94+
native/<classifier>/runtime.node and
95+
native/<classifier>/platform.properties.
96+
-->
97+
<execution>
98+
<id>jar-linux-x64</id>
99+
<phase>package</phase>
100+
<goals>
101+
<goal>jar</goal>
102+
</goals>
103+
<configuration>
104+
<classifier>${copilot.native.classifier}</classifier>
105+
<classesDirectory>${copilot.native.staging}/${copilot.native.classifier}</classesDirectory>
106+
<excludes>
107+
<exclude>.version</exclude>
108+
</excludes>
109+
</configuration>
110+
</execution>
111+
</executions>
112+
</plugin>
113+
<!--
114+
Structural guards: assert that the classifier JAR carries the
115+
native binary and that the placeholder JAR does not.
116+
-->
117+
<plugin>
118+
<groupId>org.apache.maven.plugins</groupId>
119+
<artifactId>maven-antrun-plugin</artifactId>
120+
<executions>
121+
<execution>
122+
<id>verify-native-jars</id>
123+
<phase>package</phase>
124+
<goals>
125+
<goal>run</goal>
126+
</goals>
127+
<configuration>
128+
<target>
129+
<condition property="native.binary.present">
130+
<resourceexists>
131+
<zipentry zipfile="${project.build.directory}/${project.build.finalName}-${copilot.native.classifier}.jar" name="native/${copilot.native.classifier}/runtime.node" />
132+
</resourceexists>
133+
</condition>
134+
<fail unless="native.binary.present" message="Classifier JAR is missing native/${copilot.native.classifier}/runtime.node" />
135+
<condition property="native.properties.present">
136+
<resourceexists>
137+
<zipentry zipfile="${project.build.directory}/${project.build.finalName}-${copilot.native.classifier}.jar" name="native/${copilot.native.classifier}/platform.properties" />
138+
</resourceexists>
139+
</condition>
140+
<fail unless="native.properties.present" message="Classifier JAR is missing native/${copilot.native.classifier}/platform.properties" />
141+
<condition property="placeholder.jar.polluted">
142+
<resourceexists>
143+
<zipentry zipfile="${project.build.directory}/${project.build.finalName}.jar" name="native/${copilot.native.classifier}/runtime.node" />
144+
</resourceexists>
145+
</condition>
146+
<fail if="placeholder.jar.polluted" message="Placeholder primary JAR must not contain native binaries" />
147+
</target>
148+
</configuration>
149+
</execution>
150+
</executions>
151+
</plugin>
152+
<!--
153+
Required by Maven Central: sources and javadoc artifacts. This
154+
module has no Java sources, so both produce empty archives.
155+
-->
156+
<plugin>
157+
<groupId>org.sonatype.central</groupId>
158+
<artifactId>central-publishing-maven-plugin</artifactId>
159+
<extensions>true</extensions>
160+
<configuration>
161+
<publishingServerId>central</publishingServerId>
162+
<autoPublish>true</autoPublish>
163+
</configuration>
164+
</plugin>
165+
</plugins>
166+
</build>
167+
168+
<profiles>
169+
<!--
170+
Skip the npm download when building offline or when only the
171+
placeholder JAR is needed: mvn -Dcopilot.native.skip.download=true
172+
-->
173+
<profile>
174+
<id>skip-native-download</id>
175+
<activation>
176+
<property>
177+
<name>copilot.native.skip.download</name>
178+
<value>true</value>
179+
</property>
180+
</activation>
181+
<build>
182+
<plugins>
183+
<plugin>
184+
<groupId>org.codehaus.mojo</groupId>
185+
<artifactId>exec-maven-plugin</artifactId>
186+
<configuration>
187+
<skip>true</skip>
188+
</configuration>
189+
</plugin>
190+
<plugin>
191+
<groupId>org.apache.maven.plugins</groupId>
192+
<artifactId>maven-jar-plugin</artifactId>
193+
<executions>
194+
<execution>
195+
<id>jar-linux-x64</id>
196+
<phase>none</phase>
197+
</execution>
198+
</executions>
199+
</plugin>
200+
<plugin>
201+
<groupId>org.apache.maven.plugins</groupId>
202+
<artifactId>maven-antrun-plugin</artifactId>
203+
<executions>
204+
<execution>
205+
<id>verify-native-jars</id>
206+
<phase>none</phase>
207+
</execution>
208+
</executions>
209+
</plugin>
210+
</plugins>
211+
</build>
212+
</profile>
213+
</profiles>
214+
</project>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
/**
6+
* Downloads the `runtime.node` native binary for a single platform classifier
7+
* and stages it for packaging into a classifier JAR.
8+
*
9+
* Steps:
10+
* 1. Read the pinned version and the SHA-512 `integrity` value for
11+
* `@github/copilot-<classifier>` from `nodejs/package-lock.json`.
12+
* 2. `npm pack` that exact version into the staging directory.
13+
* 3. Verify the downloaded tarball against the `integrity` value.
14+
* 4. Extract `package/prebuilds/<classifier>/runtime.node` to
15+
* `<staging>/<classifier>/native/<classifier>/runtime.node`.
16+
* 5. Write `<staging>/<classifier>/native/<classifier>/platform.properties`.
17+
*
18+
* Usage: node fetch-native.mjs <repoRoot> <stagingDir> <classifier>
19+
*/
20+
21+
import { createHash } from 'node:crypto';
22+
import { execFileSync } from 'node:child_process';
23+
import fs from 'node:fs';
24+
import path from 'node:path';
25+
26+
const [repoRoot, stagingDir, classifier] = process.argv.slice(2);
27+
28+
if (!repoRoot || !stagingDir || !classifier) {
29+
console.error('Usage: node fetch-native.mjs <repoRoot> <stagingDir> <classifier>');
30+
process.exit(1);
31+
}
32+
33+
const lockPath = path.join(repoRoot, 'nodejs', 'package-lock.json');
34+
const packageName = `@github/copilot-${classifier}`;
35+
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
36+
const entry = lock.packages?.[`node_modules/${packageName}`];
37+
38+
if (!entry?.version || !entry?.integrity) {
39+
console.error(`Could not find version/integrity for ${packageName} in ${lockPath}`);
40+
process.exit(1);
41+
}
42+
43+
const { version, integrity } = entry;
44+
if (!integrity.startsWith('sha512-')) {
45+
console.error(`Unsupported integrity algorithm for ${packageName}: ${integrity}`);
46+
process.exit(1);
47+
}
48+
49+
const outDir = path.join(stagingDir, classifier);
50+
const resourceDir = path.join(outDir, 'native', classifier);
51+
const runtimePath = path.join(resourceDir, 'runtime.node');
52+
const stampPath = path.join(outDir, '.version');
53+
54+
// Idempotence: skip the download when the staged binary already matches.
55+
// The stamp stores version + integrity + binary digest to ensure a corrupted
56+
// binary or lockfile integrity change is detected.
57+
if (fs.existsSync(runtimePath) && fs.existsSync(stampPath)) {
58+
const stampLines = fs.readFileSync(stampPath, 'utf8').trim().split('\n');
59+
const stampVersion = stampLines[0] || '';
60+
const stampIntegrity = stampLines[1] || '';
61+
const stampBinaryDigest = stampLines[2] || '';
62+
const currentBinaryDigest = `sha512-${createHash('sha512').update(fs.readFileSync(runtimePath)).digest('base64')}`;
63+
if (stampVersion === version && stampIntegrity === integrity && stampBinaryDigest === currentBinaryDigest) {
64+
console.log(`${packageName}@${version} already staged at ${runtimePath}`);
65+
process.exit(0);
66+
}
67+
}
68+
69+
fs.rmSync(outDir, { recursive: true, force: true });
70+
fs.mkdirSync(resourceDir, { recursive: true });
71+
72+
console.log(`Downloading ${packageName}@${version} ...`);
73+
const packOutput = execFileSync('npm', ['pack', `${packageName}@${version}`, '--pack-destination', outDir], {
74+
encoding: 'utf8',
75+
shell: process.platform === 'win32',
76+
});
77+
const tarballName = packOutput.trim().split('\n').pop().trim();
78+
const tarballPath = path.join(outDir, tarballName);
79+
80+
const actual = `sha512-${createHash('sha512').update(fs.readFileSync(tarballPath)).digest('base64')}`;
81+
if (actual !== integrity) {
82+
console.error(`Integrity verification failed for ${tarballPath}`);
83+
console.error(` expected: ${integrity}`);
84+
console.error(` actual: ${actual}`);
85+
process.exit(1);
86+
}
87+
console.log(`Integrity verified (${integrity.slice(0, 20)}...).`);
88+
89+
const memberPath = `package/prebuilds/${classifier}/runtime.node`;
90+
execFileSync('tar', ['-xzf', tarballPath, '-C', outDir, memberPath], { stdio: 'inherit' });
91+
fs.renameSync(path.join(outDir, memberPath), runtimePath);
92+
fs.rmSync(path.join(outDir, 'package'), { recursive: true, force: true });
93+
fs.rmSync(tarballPath, { force: true });
94+
95+
fs.writeFileSync(path.join(resourceDir, 'platform.properties'), `classifier=${classifier}\nversion=${version}\n`);
96+
const binaryDigest = `sha512-${createHash('sha512').update(fs.readFileSync(runtimePath)).digest('base64')}`;
97+
fs.writeFileSync(stampPath, `${version}\n${integrity}\n${binaryDigest}\n`);
98+
99+
console.log(`Staged ${runtimePath}`);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Placeholder marker for the primary (classifier-less) artifact of
2+
# com.github:copilot-sdk-java-runtime.
3+
#
4+
# The real native binaries ship in per-platform classifier JARs
5+
# (e.g. copilot-sdk-java-runtime-<version>-linux-x64.jar) under
6+
# native/<classifier>/runtime.node. This primary JAR exists only to satisfy
7+
# Maven Central's requirement for a main artifact and intentionally contains
8+
# no native binaries.
9+
#
10+
# This file is processed by Maven resource filtering.
11+
placeholder=true
12+
version=${project.version}

java/pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@
3838

3939
<modules>
4040
<module>sdk</module>
41+
<module>copilot-native</module>
4142
<!--
42-
copilot-native and copilot-native-all are added in later tasks (4.7+).
43-
Uncomment when the module directories and POMs are created.
43+
copilot-native-all (the monolithic uber-JAR) is added in a later task.
44+
Uncomment when the module directory and POM are created.
4445
-->
45-
<!-- <module>copilot-native</module> -->
4646
<!-- <module>copilot-native-all</module> -->
4747
</modules>
4848

0 commit comments

Comments
 (0)