-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathembeddedcli.rs
More file actions
250 lines (222 loc) · 8.88 KB
/
embeddedcli.rs
File metadata and controls
250 lines (222 loc) · 8.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
//! Lazy runtime installer for the CLI binary that build.rs embedded in this
//! crate (gated on the `bundled-cli` cargo feature, which is in the default
//! feature set).
//!
//! build.rs downloads the platform's `copilot-{platform}.{tar.gz,zip}`
//! archive from GitHub Releases, SHA-256 verifies it against the version
//! pinned in `cli-version.txt` (or `../nodejs/package-lock.json` when
//! building inside the github/copilot-sdk repo itself), and embeds the
//! **raw archive bytes**
//! into the consumer's compiled artifact via `include_bytes!()`. Extraction
//! to a real on-disk path is deferred until the first call to
//! [`path`] / [`install_at`] — at which point the bytes are part of the
//! consumer's signed binary and trusted, so no further hashing is done.
#[cfg(has_bundled_cli)]
use std::fs;
#[cfg(all(has_bundled_cli, not(windows)))]
use std::io::Read;
#[cfg(has_bundled_cli)]
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
#[cfg(has_bundled_cli)]
use tracing::{info, warn};
// When the `bundled-cli` cargo feature is enabled and the target platform is
// supported, build.rs generates `bundled_cli.rs` exposing the raw archive
// bytes. The CLI version is exposed crate-wide via the
// `cargo:rustc-env=COPILOT_SDK_CLI_VERSION` emit (see `build.rs`), and the
// binary name is OS-derived — so no other generated constants are needed.
#[cfg(has_bundled_cli)]
mod build_time {
include!(concat!(env!("OUT_DIR"), "/bundled_cli.rs"));
}
// Pinned at build time and consumed by both install paths (path/install_at).
// Sourced from the unconditional `COPILOT_SDK_CLI_VERSION` env emit in
// build.rs — the single source of truth for "what version did build.rs
// target", shared with the runtime resolver used when `bundled-cli` is off.
#[cfg(has_bundled_cli)]
const CLI_VERSION: &str = env!("COPILOT_SDK_CLI_VERSION");
// OS-derived; matches the release-archive entry name and the on-disk
// filename. No need to bake this — `cfg(windows)` reflects the target
// the runtime is running on, which by definition is the same target
// build.rs targeted.
#[cfg(all(has_bundled_cli, windows))]
const CLI_BINARY_NAME: &str = "copilot.exe";
#[cfg(all(has_bundled_cli, not(windows)))]
const CLI_BINARY_NAME: &str = "copilot";
#[cfg(feature = "bundled-cli")]
static INSTALLED_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
/// Returns the path to the installed CLI binary, lazily extracting the
/// embedded archive on first call.
///
/// On first call this extracts the embedded archive to
/// `<platform cache dir>/github-copilot-sdk/cli/<version>/copilot[.exe]`
/// and returns the resulting path. The cache dir comes from
/// [`dirs::cache_dir()`] — `%LOCALAPPDATA%` on Windows,
/// `~/Library/Caches/` on macOS, `$XDG_CACHE_HOME` (or `~/.cache/`) on
/// Linux. Subsequent calls return the cached result. The extraction
/// is skipped when the target file already exists — the per-version
/// install directory and the assumption that the consumer's binary is
/// trusted mean no further hashing is needed.
///
/// Returns `None` if no CLI was embedded at build time.
#[cfg(feature = "bundled-cli")]
pub(crate) fn path() -> Option<PathBuf> {
INSTALLED_PATH
.get_or_init(|| {
#[cfg(has_bundled_cli)]
{
let dir = default_install_dir(CLI_VERSION);
match install(&dir, build_time::CLI_ARCHIVE) {
Ok(path) => {
info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed");
return Some(path);
}
Err(e) => {
warn!(error = %e, "embedded CLI installation failed");
}
}
}
None
})
.clone()
}
/// Install the embedded CLI binary into the given directory instead of the
/// default `<platform cache dir>/github-copilot-sdk/cli/<version>/` location
/// (see [`path`] for the per-platform mapping).
///
/// Idempotent: skips extraction if the target binary already exists.
/// Returns `None` when the SDK was built without a bundled CLI.
#[cfg(feature = "bundled-cli")]
#[allow(dead_code)] // Used by resolve.rs when ClientOptions::bundled_cli_extract_dir is set.
pub(crate) fn install_at(extract_dir: &Path) -> Option<PathBuf> {
#[cfg(has_bundled_cli)]
{
match install(extract_dir, build_time::CLI_ARCHIVE) {
Ok(path) => {
info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed");
return Some(path);
}
Err(e) => {
warn!(error = %e, "embedded CLI installation failed");
}
}
}
#[cfg(not(has_bundled_cli))]
{
let _ = extract_dir;
}
None
}
#[cfg(has_bundled_cli)]
fn default_install_dir(version: &str) -> PathBuf {
let cache = dirs::cache_dir().unwrap_or_else(std::env::temp_dir);
let root = cache.join("github-copilot-sdk").join("cli");
if version.is_empty() {
root.join("unversioned")
} else {
root.join(sanitize_version(version))
}
}
#[cfg(has_bundled_cli)]
fn install(install_dir: &Path, archive: &[u8]) -> Result<PathBuf, EmbeddedCliError> {
let verbose = std::env::var("COPILOT_CLI_INSTALL_VERBOSE").ok().as_deref() == Some("1");
fs::create_dir_all(install_dir).map_err(EmbeddedCliError::CreateDir)?;
let final_path = install_dir.join(CLI_BINARY_NAME);
// Per-version install dir means a present file at this path is the
// binary we want — no need to hash-verify the bytes are unchanged.
if final_path.is_file() {
if verbose {
eprintln!("embedded CLI already installed at {}", final_path.display());
}
return Ok(final_path);
}
let start = std::time::Instant::now();
let bytes = extract_binary(archive, CLI_BINARY_NAME)?;
write_binary(&final_path, &bytes)?;
if verbose {
eprintln!(
"embedded CLI extracted to {} in {:?}",
final_path.display(),
start.elapsed()
);
}
Ok(final_path)
}
#[cfg(all(has_bundled_cli, not(windows)))]
fn extract_binary(archive: &[u8], binary_name: &str) -> Result<Vec<u8>, EmbeddedCliError> {
let gz = flate2::read::GzDecoder::new(archive);
let mut tar = tar::Archive::new(gz);
for entry in tar.entries().map_err(EmbeddedCliError::Archive)? {
let mut entry = entry.map_err(EmbeddedCliError::Archive)?;
let path = entry.path().map_err(EmbeddedCliError::Archive)?;
let name = path.to_string_lossy();
if name == binary_name || name.ends_with(&format!("/{binary_name}")) {
let mut bytes = Vec::with_capacity(entry.size() as usize);
entry
.read_to_end(&mut bytes)
.map_err(EmbeddedCliError::Archive)?;
return Ok(bytes);
}
}
Err(EmbeddedCliError::BinaryNotFoundInArchive)
}
#[cfg(all(has_bundled_cli, windows))]
fn extract_binary(archive: &[u8], binary_name: &str) -> Result<Vec<u8>, EmbeddedCliError> {
let cursor = std::io::Cursor::new(archive);
let mut zip = zip::ZipArchive::new(cursor).map_err(EmbeddedCliError::Zip)?;
for i in 0..zip.len() {
let mut entry = zip.by_index(i).map_err(EmbeddedCliError::Zip)?;
let name = entry.name().to_string();
if name == binary_name || name.ends_with(&format!("/{binary_name}")) {
let mut bytes = Vec::with_capacity(entry.size() as usize);
std::io::copy(&mut entry, &mut bytes).map_err(EmbeddedCliError::Io)?;
return Ok(bytes);
}
}
Err(EmbeddedCliError::BinaryNotFoundInArchive)
}
#[cfg(has_bundled_cli)]
fn sanitize_version(version: &str) -> String {
version
.chars()
.map(|c| match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' => c,
_ => '_',
})
.collect()
}
#[cfg(has_bundled_cli)]
fn write_binary(path: &Path, data: &[u8]) -> Result<(), EmbeddedCliError> {
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(EmbeddedCliError::Io)?;
file.write_all(data).map_err(EmbeddedCliError::Io)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o755))
.map_err(EmbeddedCliError::Io)?;
}
Ok(())
}
#[cfg(has_bundled_cli)]
#[derive(Debug, thiserror::Error)]
#[allow(dead_code)]
enum EmbeddedCliError {
#[error("failed to create install directory: {0}")]
CreateDir(io::Error),
#[cfg(not(windows))]
#[error("failed to read archive entry: {0}")]
Archive(io::Error),
#[cfg(windows)]
#[error("failed to read zip archive: {0}")]
Zip(zip::result::ZipError),
#[error("CLI binary not found in embedded archive")]
BinaryNotFoundInArchive,
#[error("I/O error: {0}")]
Io(io::Error),
}