#[cfg(any(has_bundled_cli, test))]
use std::collections::BTreeMap;
#[cfg(has_bundled_cli)]
use std::collections::HashSet;
#[cfg(any(has_bundled_cli, test))]
use std::fs;
#[cfg(has_bundled_cli)]
use std::io::Read;
#[cfg(any(has_bundled_cli, test))]
use std::io::Write;
use std::path::{Path, PathBuf};
#[cfg(any(has_bundled_cli, test))]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(any(has_bundled_cli, test))]
use std::sync::{Arc, Mutex};
#[cfg(any(has_bundled_cli, test))]
use std::time::{Duration, Instant};
#[cfg(has_bundled_cli)]
use tracing::{info, warn};
#[cfg(has_bundled_cli)]
mod build_time {
include!(concat!(env!("OUT_DIR"), "/bundled_cli.rs"));
}
#[cfg(has_bundled_cli)]
const CLI_VERSION: &str = env!("COPILOT_SDK_CLI_VERSION");
#[cfg(has_bundled_cli)]
const CLI_CACHE_ID: &str = env!("COPILOT_SDK_CLI_CACHE_ID");
#[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(all(has_bundled_cli, windows))]
const RUNTIME_BINARY_NAME: &str = "copilot-runtime.exe";
#[cfg(all(has_bundled_cli, not(windows)))]
const RUNTIME_BINARY_NAME: &str = "copilot-runtime";
#[cfg(has_bundled_cli)]
const RUNTIME_NODE_NAME: &str = "runtime.node";
#[cfg(any(has_bundled_cli, test))]
const RUNTIME_VERSION_MARKER: &str = ".copilot-runtime-version";
#[cfg(any(has_bundled_cli, test))]
const RUNTIME_VERSION_MARKER_WAIT: Duration = Duration::from_secs(1);
#[cfg(has_bundled_cli)]
static INSTALLED_PATH: Mutex<Option<PathBuf>> = Mutex::new(None);
#[cfg(has_bundled_cli)]
static INSTALLED_RUNTIME_PATH: Mutex<Option<PathBuf>> = Mutex::new(None);
#[cfg(has_bundled_cli)]
static CLI_INSTALLS_AT: DirInstalls = Mutex::new(BTreeMap::new());
#[cfg(has_bundled_cli)]
static RUNTIME_INSTALLS_AT: DirInstalls = Mutex::new(BTreeMap::new());
#[cfg(any(has_bundled_cli, test))]
type DirInstalls = Mutex<BTreeMap<PathBuf, Arc<DirInstall>>>;
#[cfg(any(has_bundled_cli, test))]
#[derive(Default)]
struct DirInstall {
finished: AtomicU64,
published: Mutex<Option<PathBuf>>,
}
#[cfg(any(has_bundled_cli, test))]
struct DirInstallGuard<'a> {
installs: &'a DirInstalls,
key: PathBuf,
entry: Arc<DirInstall>,
}
#[cfg(any(has_bundled_cli, test))]
impl Drop for DirInstallGuard<'_> {
fn drop(&mut self) {
let mut installs = self
.installs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if installs
.get(&self.key)
.is_some_and(|entry| Arc::ptr_eq(entry, &self.entry))
&& Arc::strong_count(&self.entry) == 2
{
installs.remove(&self.key);
}
}
}
#[cfg(any(has_bundled_cli, test))]
impl DirInstall {
fn for_dir<'a>(installs: &'a DirInstalls, dir: &Path) -> DirInstallGuard<'a> {
let _ = fs::create_dir_all(dir);
let key = fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
let entry = installs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entry(key.clone())
.or_default()
.clone();
DirInstallGuard {
installs,
key,
entry,
}
}
fn run(
&self,
arrived: u64,
install: impl FnOnce() -> Result<PathBuf, crate::Error>,
) -> Result<PathBuf, crate::Error> {
let mut published = self
.published
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.finished.load(Ordering::Acquire) != arrived
&& let Some(path) = published.as_ref()
{
return Ok(path.clone());
}
let result = install().and_then(|path| std::path::absolute(path).map_err(Into::into));
*published = result.as_ref().ok().cloned();
self.finished.fetch_add(1, Ordering::Release);
result
}
}
#[cfg(any(has_bundled_cli, test))]
fn install_once_at(
installs: &DirInstalls,
dir: &Path,
install: impl FnOnce() -> Result<PathBuf, crate::Error>,
) -> Result<PathBuf, crate::Error> {
let entry = DirInstall::for_dir(installs, dir);
let arrived = entry.entry.finished.load(Ordering::Acquire);
entry.entry.run(arrived, install)
}
#[cfg(any(has_bundled_cli, test))]
fn cached_install(
cache: &Mutex<Option<PathBuf>>,
select_dir: impl FnOnce() -> Result<PathBuf, crate::ErrorKind>,
install: impl FnOnce(&Path) -> Result<PathBuf, crate::Error>,
) -> Result<PathBuf, crate::Error> {
let mut installed = cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(path) = installed.as_ref() {
return Ok(path.clone());
}
let dir = select_dir()?;
let path = install(&dir)?;
*installed = Some(path.clone());
Ok(path)
}
#[cfg(any(has_bundled_cli, test))]
fn install_failed(binary_name: &str, dir: &Path, error: &EmbeddedCliError) -> crate::Error {
crate::ErrorKind::BinaryNotFound {
name: binary_name.into(),
hint: Some(format!(
"the bundled Copilot CLI could not be installed in '{}': {error}",
dir.display()
)),
}
.into()
}
#[cfg(feature = "bundled-cli")]
pub(crate) fn path() -> Result<Option<PathBuf>, crate::Error> {
#[cfg(has_bundled_cli)]
{
cached_install(
&INSTALLED_PATH,
|| {
default_install_dir(CLI_CACHE_ID).map_err(|error| {
crate::ErrorKind::BinaryNotFound {
name: CLI_BINARY_NAME.into(),
hint: Some(error.to_string()),
}
})
},
try_install_cli,
)
.map(Some)
}
#[cfg(not(has_bundled_cli))]
{
Ok(None)
}
}
#[cfg(feature = "bundled-cli")]
#[allow(dead_code)] pub(crate) fn install_at(extract_dir: &Path) -> Result<Option<PathBuf>, crate::Error> {
#[cfg(has_bundled_cli)]
{
install_once_at(&CLI_INSTALLS_AT, extract_dir, || {
try_install_cli(extract_dir)
})
.map(Some)
}
#[cfg(not(has_bundled_cli))]
{
let _ = extract_dir;
Ok(None)
}
}
#[cfg(has_bundled_cli)]
fn try_install_cli(dir: &Path) -> Result<PathBuf, crate::Error> {
let path = install_cli(dir, build_time::CLI_ARCHIVE, build_time::CLI_BINARY_SIZE)
.map_err(|error| install_failed(CLI_BINARY_NAME, dir, &error))?;
info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed");
Ok(path)
}
#[cfg(feature = "bundled-cli")]
pub(crate) fn runtime_path() -> Result<Option<PathBuf>, crate::Error> {
#[cfg(has_bundled_cli)]
{
cached_install(
&INSTALLED_RUNTIME_PATH,
|| {
default_install_dir(CLI_CACHE_ID).map_err(|error| {
crate::ErrorKind::BinaryNotFound {
name: RUNTIME_BINARY_NAME.into(),
hint: Some(error.to_string()),
}
})
},
try_install_runtime,
)
.map(Some)
}
#[cfg(not(has_bundled_cli))]
{
Ok(None)
}
}
#[cfg(feature = "bundled-cli")]
pub(crate) fn install_runtime_at(extract_dir: &Path) -> Result<Option<PathBuf>, crate::Error> {
#[cfg(has_bundled_cli)]
{
install_once_at(&RUNTIME_INSTALLS_AT, extract_dir, || {
let install_dir = runtime_install_dir(extract_dir, CLI_CACHE_ID)
.map_err(|error| install_failed(RUNTIME_BINARY_NAME, extract_dir, &error))?;
try_install_runtime(&install_dir)
})
.map(Some)
}
#[cfg(not(has_bundled_cli))]
{
let _ = extract_dir;
Ok(None)
}
}
#[cfg(has_bundled_cli)]
fn try_install_runtime(dir: &Path) -> Result<PathBuf, crate::Error> {
let path = install_runtime(dir, build_time::RUNTIME_ARCHIVE)
.map_err(|error| install_failed(RUNTIME_BINARY_NAME, dir, &error))?;
info!(path = %path.display(), version = CLI_VERSION, "embedded runtime installed");
Ok(path)
}
#[cfg(any(has_bundled_cli, test))]
fn runtime_install_dir(base_dir: &Path, version: &str) -> Result<PathBuf, EmbeddedCliError> {
runtime_install_dir_within(base_dir, version, RUNTIME_VERSION_MARKER_WAIT)
}
#[cfg(any(has_bundled_cli, test))]
fn runtime_install_dir_within(
base_dir: &Path,
version: &str,
wait: Duration,
) -> Result<PathBuf, EmbeddedCliError> {
fs::create_dir_all(base_dir)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?;
let deadline = Instant::now() + wait;
loop {
if let Some(dir) = try_runtime_install_dir(base_dir, version)? {
return Ok(dir);
}
if runtime_marker_is_stale(&base_dir.join(RUNTIME_VERSION_MARKER), wait)
|| Instant::now() >= deadline
{
return Ok(base_dir.join(version));
}
std::thread::sleep(Duration::from_millis(5));
}
}
#[cfg(any(has_bundled_cli, test))]
fn try_runtime_install_dir(
base_dir: &Path,
version: &str,
) -> Result<Option<PathBuf>, EmbeddedCliError> {
let marker = base_dir.join(RUNTIME_VERSION_MARKER);
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&marker)
{
Ok(mut file) => {
if let Err(error) = file
.write_all(version.as_bytes())
.and_then(|()| file.sync_all())
{
drop(file);
let _ = fs::remove_file(&marker);
return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, error));
}
Ok(Some(base_dir.to_path_buf()))
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
match fs::read_to_string(&marker) {
Ok(installed) if !installed.is_empty() || version.is_empty() => {
Ok(Some(if installed == version {
base_dir.to_path_buf()
} else {
base_dir.join(version)
}))
}
Ok(_) => Ok(None),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(_) => Ok(Some(base_dir.join(version))),
}
}
Err(error) => Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, error)),
}
}
#[cfg(any(has_bundled_cli, test))]
fn runtime_marker_is_stale(marker: &Path, wait: Duration) -> bool {
fs::metadata(marker)
.and_then(|metadata| metadata.modified())
.ok()
.and_then(|modified| modified.elapsed().ok())
.is_some_and(|age| age >= wait)
}
#[cfg(has_bundled_cli)]
fn default_install_dir(version: &str) -> std::io::Result<PathBuf> {
Ok(crate::cache_paths::cache_install_dir(
&crate::cache_paths::platform_cache_root()?,
"cli",
if version.is_empty() {
"unversioned"
} else {
version
},
))
}
#[cfg(has_bundled_cli)]
const MAX_PUBLISH_ATTEMPTS: u32 = 3;
#[cfg(all(has_bundled_cli, feature = "in-process", windows))]
const RUNTIME_LIBRARY_NAME: &str = "copilot_runtime.dll";
#[cfg(all(has_bundled_cli, feature = "in-process", target_os = "macos"))]
const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.dylib";
#[cfg(all(
has_bundled_cli,
feature = "in-process",
not(windows),
not(target_os = "macos")
))]
const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.so";
#[cfg(has_bundled_cli)]
fn install_runtime(install_dir: &Path, archive: &[u8]) -> Result<PathBuf, EmbeddedCliError> {
fs::create_dir_all(install_dir)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?;
let root = fs::canonicalize(install_dir)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?;
let mut required = vec![RUNTIME_BINARY_NAME, RUNTIME_NODE_NAME];
#[cfg(feature = "in-process")]
required.push(RUNTIME_LIBRARY_NAME);
let mut seen = HashSet::new();
let mut changed = HashSet::new();
let mut pending = Vec::new();
let mut tar = tar::Archive::new(flate2::read::GzDecoder::new(archive));
for entry in tar
.entries()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?
{
let mut entry =
entry.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
if !entry.header().entry_type().is_file() {
continue;
}
let path = entry
.path()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
let path = runtime_asset_path(&path)?;
if !seen.insert(path.as_os_str().to_ascii_lowercase()) {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Archive,
format!("duplicate embedded runtime asset path: {}", path.display()),
));
}
if !selected_runtime_asset(&path) {
continue;
}
if let Some(index) = required.iter().position(|name| path == Path::new(name)) {
if entry.size() == 0 {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
format!("embedded runtime artifact is empty: {}", path.display()),
));
}
required.remove(index);
}
let target = root.join(&path);
let parent = target.parent().ok_or_else(|| {
EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Archive,
format!("embedded runtime asset has no parent: {}", path.display()),
)
})?;
check_runtime_asset_parent(&root, parent, false)?;
match existing_runtime_file(&target, entry.size())? {
Some(mut installed) => {
let matches = runtime_entry_matches(&mut entry, &mut installed)?;
#[cfg(unix)]
let matches = matches
&& (path != Path::new(RUNTIME_BINARY_NAME)
|| check_runtime_wrapper_execute_access(&target).is_ok());
if !matches {
changed.insert(path);
}
}
None => {
check_runtime_asset_parent(&root, parent, true)?;
pending.push(stage_runtime_entry(&mut entry, &target)?);
}
}
}
std::io::copy(&mut tar.into_inner(), &mut std::io::sink())
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
if !required.is_empty() {
return Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into());
}
if !changed.is_empty() {
let mut tar = tar::Archive::new(flate2::read::GzDecoder::new(archive));
for entry in tar
.entries()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?
{
let mut entry =
entry.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
if !entry.header().entry_type().is_file() {
continue;
}
let path = entry
.path()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
let path = runtime_asset_path(&path)?;
if changed.contains(&path) {
pending.push(stage_runtime_entry(&mut entry, &root.join(path))?);
}
}
std::io::copy(&mut tar.into_inner(), &mut std::io::sink())
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
}
for staged in pending {
publish_staged(
&staged.temporary,
&staged.target,
staged.target == root.join(RUNTIME_BINARY_NAME),
)?;
}
#[cfg(unix)]
check_runtime_wrapper_execute_access(&root.join(RUNTIME_BINARY_NAME))
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Verification, e))?;
Ok(install_dir.join(RUNTIME_BINARY_NAME))
}
#[cfg(all(any(has_bundled_cli, test), unix))]
fn check_runtime_wrapper_execute_access(path: &Path) -> std::io::Result<()> {
use std::ffi::{CString, c_char, c_int};
use std::os::unix::ffi::OsStrExt;
#[cfg(target_os = "linux")]
const AT_FDCWD: c_int = -100;
#[cfg(target_os = "macos")]
const AT_FDCWD: c_int = -2;
#[cfg(target_os = "linux")]
const AT_EACCESS: c_int = 0x200;
#[cfg(target_os = "macos")]
const AT_EACCESS: c_int = 0x10;
const X_OK: c_int = 1;
unsafe extern "C" {
fn faccessat(dirfd: c_int, path: *const c_char, mode: c_int, flags: c_int) -> c_int;
}
let path = CString::new(path.as_os_str().as_bytes())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
if unsafe { faccessat(AT_FDCWD, path.as_ptr(), X_OK, AT_EACCESS) } == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(has_bundled_cli)]
fn selected_runtime_asset(path: &Path) -> bool {
if path == Path::new(CLI_BINARY_NAME) {
return false;
}
if matches!(
path.file_name().and_then(|name| name.to_str()),
Some("copilot_runtime.dll" | "libcopilot_runtime.dylib" | "libcopilot_runtime.so")
) {
#[cfg(feature = "in-process")]
return path == Path::new(RUNTIME_LIBRARY_NAME);
#[cfg(not(feature = "in-process"))]
return false;
}
true
}
#[cfg(has_bundled_cli)]
fn runtime_asset_path(path: &Path) -> Result<PathBuf, EmbeddedCliError> {
let invalid = || {
EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Archive,
format!(
"non-portable embedded runtime asset path: {}",
path.display()
),
)
};
let name = path
.to_str()
.filter(|name| name.is_ascii())
.ok_or_else(invalid)?;
if name.starts_with(['/', '\\']) {
return Err(invalid());
}
let mut normalized = PathBuf::new();
for component in name.split(['/', '\\']) {
if component.is_empty() || component == "." {
continue;
}
if component == ".."
|| component.ends_with(['.', ' '])
|| component
.bytes()
.any(|byte| byte.is_ascii_control() || b"<>:\"|?*~".contains(&byte))
{
return Err(invalid());
}
let stem = component
.split('.')
.next()
.expect("nonempty component")
.trim_end_matches(' ')
.to_ascii_uppercase();
if matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL")
|| (stem.len() == 4
&& (stem.starts_with("COM") || stem.starts_with("LPT"))
&& matches!(stem.as_bytes()[3], b'1'..=b'9'))
{
return Err(invalid());
}
normalized.push(component);
}
if normalized.as_os_str().is_empty() {
return Err(invalid());
}
Ok(normalized)
}
#[cfg(has_bundled_cli)]
fn check_runtime_asset_parent(
root: &Path,
parent: &Path,
create: bool,
) -> Result<(), EmbeddedCliError> {
let mut current = root.to_path_buf();
for component in parent
.strip_prefix(root)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?
.components()
{
current.push(component);
if create {
match fs::create_dir(¤t) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(e) => return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e)),
}
}
let metadata = match fs::symlink_metadata(¤t) {
Ok(metadata) => metadata,
Err(e) if !create && e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e)),
};
if !metadata.is_dir() {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
format!(
"runtime asset parent is not a directory: {}",
current.display()
),
));
}
}
Ok(())
}
#[cfg(has_bundled_cli)]
struct StagedRuntimeFile {
temporary: PathBuf,
target: PathBuf,
}
#[cfg(has_bundled_cli)]
impl Drop for StagedRuntimeFile {
fn drop(&mut self) {
if let Err(error) = fs::remove_file(&self.temporary)
&& error.kind() != std::io::ErrorKind::NotFound
{
warn!(path = %self.temporary.display(), %error, "failed to remove staged runtime asset");
}
}
}
#[cfg(has_bundled_cli)]
fn existing_runtime_file(target: &Path, size: u64) -> Result<Option<fs::File>, EmbeddedCliError> {
let metadata = match fs::symlink_metadata(target) {
Ok(metadata) if metadata.is_file() => Some(metadata),
Ok(_) => {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
format!("runtime asset is not a regular file: {}", target.display()),
));
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e)),
};
let matches = metadata
.as_ref()
.is_some_and(|metadata| metadata.len() == size);
if matches {
match fs::File::open(target) {
Ok(file) => return Ok(Some(file)),
Err(e) => {
tracing::debug!(path = %target.display(), error = %e,
"existing runtime asset cannot be read; repairing");
}
}
}
Ok(None)
}
#[cfg(has_bundled_cli)]
fn runtime_entry_matches<R: Read>(
entry: &mut tar::Entry<'_, R>,
installed: &mut fs::File,
) -> Result<bool, EmbeddedCliError> {
let mut buffer = [0u8; 64 * 1024];
let mut on_disk = [0u8; 64 * 1024];
let mut remaining = entry.size();
while remaining > 0 {
let length = remaining.min(buffer.len() as u64) as usize;
entry
.read_exact(&mut buffer[..length])
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
remaining -= length as u64;
if let Err(error) = installed.read_exact(&mut on_disk[..length]) {
tracing::debug!(%error, "existing runtime asset cannot be read; repairing");
return Ok(false);
}
if on_disk[..length] != buffer[..length] {
return Ok(false);
}
}
match installed.read(&mut on_disk[..1]) {
Ok(read) => Ok(read == 0),
Err(error) => {
tracing::debug!(%error, "existing runtime asset cannot be read; repairing");
Ok(false)
}
}
}
#[cfg(has_bundled_cli)]
fn stage_runtime_entry<R: Read>(
entry: &mut tar::Entry<'_, R>,
target: &Path,
) -> Result<StagedRuntimeFile, EmbeddedCliError> {
let parent = target.parent().expect("runtime asset has a checked parent");
let (temporary, mut file) = create_temp_file(parent)?;
let staged = StagedRuntimeFile {
temporary,
target: target.to_path_buf(),
};
let result = write_runtime_entry(entry, &mut file);
drop(file);
result?;
Ok(staged)
}
#[cfg(has_bundled_cli)]
fn write_runtime_entry<R: Read>(
entry: &mut tar::Entry<'_, R>,
file: &mut fs::File,
) -> Result<(), EmbeddedCliError> {
let size = entry.size();
let written = std::io::copy(&mut (&mut *entry).take(size), &mut *file)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?;
if written != size {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
format!("runtime entry size mismatch: read {written} bytes, expected {size}"),
));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = entry
.header()
.mode()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?
& 0o777;
file.set_permissions(fs::Permissions::from_mode(mode))
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?;
}
file.sync_all()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))
}
#[cfg(has_bundled_cli)]
fn install_cli(
install_dir: &Path,
archive: &[u8],
expected_binary_size: u64,
) -> 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(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?;
let final_path = install_dir.join(CLI_BINARY_NAME);
let marker_path = marker_path(install_dir);
if existing_install_is_valid(&final_path, &marker_path, expected_binary_size) {
if verbose {
eprintln!("embedded CLI already installed at {}", final_path.display());
}
return Ok(final_path);
}
let start = std::time::Instant::now();
let bytes = extract_cli_binary(archive)?;
if bytes.is_empty() {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
"extracted CLI binary is empty",
));
}
let mut last_err: Option<EmbeddedCliError> = None;
for attempt in 1..=MAX_PUBLISH_ATTEMPTS {
match publish_verified(install_dir, &final_path, &marker_path, &bytes) {
Ok(()) => {
if verbose {
eprintln!(
"embedded CLI extracted to {} in {:?}",
final_path.display(),
start.elapsed()
);
}
return Ok(final_path);
}
Err(e) => {
if verify_on_disk_matches(&final_path, &bytes).is_ok() {
let _ = write_marker(&marker_path, bytes.len() as u64);
return Ok(final_path);
}
warn!(attempt, error = %e, "embedded CLI publish attempt failed; retrying");
last_err = Some(e);
}
}
}
Err(EmbeddedCliError::with_source(
EmbeddedCliErrorKind::Blocked,
last_err,
))
}
#[cfg(any(has_bundled_cli, test))]
fn marker_path(install_dir: &Path) -> PathBuf {
install_dir.join(".copilot-cli.ok")
}
#[cfg(any(has_bundled_cli, test))]
fn existing_install_is_valid(
final_path: &Path,
marker_path: &Path,
expected_binary_size: u64,
) -> bool {
let Ok(meta) = fs::metadata(final_path) else {
return false;
};
if !meta.is_file() || meta.len() == 0 {
return false;
}
match read_marker_len(marker_path) {
Some(expected) if expected == expected_binary_size && expected == meta.len() => {
looks_like_valid_image(final_path)
}
_ => false,
}
}
#[cfg(any(has_bundled_cli, test))]
fn publish_verified(
install_dir: &Path,
final_path: &Path,
marker_path: &Path,
bytes: &[u8],
) -> Result<(), EmbeddedCliError> {
let tmp = write_temp_file(install_dir, bytes)?;
if let Err(e) = verify_on_disk_matches(&tmp, bytes) {
let _ = fs::remove_file(&tmp);
return Err(e);
}
if let Err(e) = publish_staged(&tmp, final_path, true) {
let _ = fs::remove_file(&tmp);
return Err(e);
}
verify_on_disk_matches(final_path, bytes)?;
write_marker(marker_path, bytes.len() as u64)?;
Ok(())
}
#[cfg(any(has_bundled_cli, test))]
fn write_temp_file(dir: &Path, contents: &[u8]) -> Result<PathBuf, EmbeddedCliError> {
let (tmp, mut file) = create_temp_file(dir)?;
if let Err(e) = file
.write_all(contents)
.and_then(|()| file.flush())
.and_then(|()| file.sync_all())
{
drop(file);
let _ = fs::remove_file(&tmp);
return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(e) = fs::set_permissions(&tmp, fs::Permissions::from_mode(0o755)) {
drop(file);
let _ = fs::remove_file(&tmp);
return Err(EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e));
}
}
drop(file);
Ok(tmp)
}
#[cfg(any(has_bundled_cli, test))]
fn create_temp_file(dir: &Path) -> Result<(PathBuf, fs::File), EmbeddedCliError> {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let tmp = dir.join(format!(
".copilot-cli.tmp.{}.{}.{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed),
nanos
));
let file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?;
Ok((tmp, file))
}
#[cfg(any(has_bundled_cli, test))]
fn publish(tmp: &Path, final_path: &Path) -> Result<(), EmbeddedCliError> {
fs::rename(tmp, final_path).map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Publish, e))
}
#[cfg(any(has_bundled_cli, test))]
fn publish_staged(staged: &Path, target: &Path, executable: bool) -> Result<(), EmbeddedCliError> {
#[cfg(not(unix))]
let _ = executable;
#[cfg(unix)]
{
let in_place = match fs::hard_link(staged, target) {
Ok(()) => true,
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
same_contents_and_mode(staged, target)
&& (!executable || check_runtime_wrapper_execute_access(target).is_ok())
}
Err(e) => {
tracing::debug!(path = %target.display(), error = %e,
"cannot hard-link the staged file; replacing the target");
false
}
};
if in_place {
if let Err(e) = fs::remove_file(staged)
&& e.kind() != std::io::ErrorKind::NotFound
{
tracing::debug!(path = %staged.display(), error = %e,
"failed to remove the staged file");
}
return Ok(());
}
}
publish(staged, target)
}
#[cfg(all(any(has_bundled_cli, test), unix))]
fn same_contents_and_mode(staged: &Path, target: &Path) -> bool {
use std::io::Read as _;
use std::os::unix::fs::PermissionsExt;
let (Ok(staged_meta), Ok(target_meta)) = (fs::metadata(staged), fs::symlink_metadata(target))
else {
return false;
};
if !target_meta.is_file()
|| staged_meta.len() != target_meta.len()
|| staged_meta.permissions().mode() & 0o777 != target_meta.permissions().mode() & 0o777
{
return false;
}
let (Ok(mut staged), Ok(mut target)) = (fs::File::open(staged), fs::File::open(target)) else {
return false;
};
let mut staged_buf = [0u8; 64 * 1024];
let mut target_buf = [0u8; 64 * 1024];
loop {
let Ok(read) = staged.read(&mut staged_buf) else {
return false;
};
if read == 0 {
return matches!(target.read(&mut target_buf[..1]), Ok(0));
}
if target.read_exact(&mut target_buf[..read]).is_err()
|| staged_buf[..read] != target_buf[..read]
{
return false;
}
}
}
#[cfg(any(has_bundled_cli, test))]
fn verify_on_disk_matches(path: &Path, expected: &[u8]) -> Result<(), EmbeddedCliError> {
let actual = fs::read(path).map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))?;
if actual.len() != expected.len() {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
format!(
"size mismatch: on-disk {} bytes, expected {} bytes",
actual.len(),
expected.len()
),
));
}
if actual != expected {
return Err(EmbeddedCliError::with_message(
EmbeddedCliErrorKind::Verification,
"on-disk binary differs from the embedded image",
));
}
Ok(())
}
#[cfg(any(has_bundled_cli, test))]
fn looks_like_valid_image(path: &Path) -> bool {
use std::io::Read as _;
let mut buf = [0u8; 4];
let Ok(mut file) = fs::File::open(path) else {
return false;
};
let Ok(read) = file.read(&mut buf) else {
return false;
};
let head = &buf[..read];
#[cfg(windows)]
{
head.starts_with(b"MZ")
}
#[cfg(target_os = "macos")]
{
matches!(
head,
[0xfe, 0xed, 0xfa, 0xce] | [0xfe, 0xed, 0xfa, 0xcf] | [0xce, 0xfa, 0xed, 0xfe] | [0xcf, 0xfa, 0xed, 0xfe] | [0xca, 0xfe, 0xba, 0xbe] | [0xbe, 0xba, 0xfe, 0xca] )
}
#[cfg(all(not(windows), not(target_os = "macos")))]
{
head.starts_with(b"\x7fELF")
}
}
#[cfg(any(has_bundled_cli, test))]
fn write_marker(marker_path: &Path, size: u64) -> Result<(), EmbeddedCliError> {
fs::write(marker_path, size.to_string())
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Io, e))
}
#[cfg(any(has_bundled_cli, test))]
fn read_marker_len(marker_path: &Path) -> Option<u64> {
fs::read_to_string(marker_path)
.ok()?
.trim()
.parse::<u64>()
.ok()
}
#[cfg(all(has_bundled_cli, not(windows)))]
fn extract_cli_binary(archive: &[u8]) -> Result<Vec<u8>, EmbeddedCliError> {
extract_binary(archive, CLI_BINARY_NAME)
}
#[cfg(all(has_bundled_cli, windows))]
fn extract_cli_binary(archive: &[u8]) -> Result<Vec<u8>, EmbeddedCliError> {
let reader = std::io::Cursor::new(archive);
let mut zip = zip::ZipArchive::new(reader)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
for index in 0..zip.len() {
let mut entry = zip
.by_index(index)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
let name = entry
.name()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
if name == CLI_BINARY_NAME || name.ends_with(&format!("/{CLI_BINARY_NAME}")) {
let mut bytes = Vec::with_capacity(entry.size() as usize);
entry
.read_to_end(&mut bytes)
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
return Ok(bytes);
}
}
Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into())
}
#[cfg(all(has_bundled_cli, any(not(windows), test)))]
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(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?
{
let mut entry =
entry.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
let path = entry
.path()
.map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
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(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?;
return Ok(bytes);
}
}
Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into())
}
#[cfg(any(has_bundled_cli, test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(dead_code)]
enum EmbeddedCliErrorKind {
CreateDir,
Archive,
BinaryNotFoundInArchive,
Io,
Publish,
Verification,
Blocked,
}
#[cfg(any(has_bundled_cli, test))]
impl std::fmt::Display for EmbeddedCliErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EmbeddedCliErrorKind::CreateDir => f.write_str("failed to create install directory"),
EmbeddedCliErrorKind::Archive => f.write_str("failed to read archive entry"),
EmbeddedCliErrorKind::BinaryNotFoundInArchive => {
f.write_str("CLI binary not found in embedded archive")
}
EmbeddedCliErrorKind::Io => f.write_str("I/O error"),
EmbeddedCliErrorKind::Publish => {
f.write_str("failed to publish the extracted CLI binary")
}
EmbeddedCliErrorKind::Verification => {
f.write_str("extracted CLI binary failed integrity verification")
}
EmbeddedCliErrorKind::Blocked => f.write_str(
"bundled CLI appears blocked or corrupt after multiple attempts \
(possibly quarantined by antivirus)",
),
}
}
}
#[cfg(any(has_bundled_cli, test))]
#[allow(dead_code)]
struct EmbeddedCliError {
repr: crate::errors::Repr<EmbeddedCliErrorKind>,
}
#[cfg(any(has_bundled_cli, test))]
#[allow(dead_code)]
impl EmbeddedCliError {
fn new<E>(kind: EmbeddedCliErrorKind, error: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Self {
repr: crate::errors::Repr::Custom(crate::errors::Custom {
kind,
error: error.into(),
}),
}
}
fn with_message(
kind: EmbeddedCliErrorKind,
message: impl Into<std::borrow::Cow<'static, str>>,
) -> Self {
Self {
repr: crate::errors::Repr::SimpleMessage(kind, message.into()),
}
}
fn with_source(kind: EmbeddedCliErrorKind, source: Option<EmbeddedCliError>) -> Self {
match source {
Some(source) => Self::new(kind, Box::new(source)),
None => Self {
repr: crate::errors::Repr::Simple(kind),
},
}
}
}
#[cfg(any(has_bundled_cli, test))]
impl From<EmbeddedCliErrorKind> for EmbeddedCliError {
fn from(kind: EmbeddedCliErrorKind) -> Self {
Self {
repr: crate::errors::Repr::Simple(kind),
}
}
}
#[cfg(any(has_bundled_cli, test))]
impl std::fmt::Display for EmbeddedCliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.repr {
crate::errors::Repr::Simple(kind) => write!(f, "{kind}"),
crate::errors::Repr::SimpleMessage(_, msg) => write!(f, "{msg}"),
crate::errors::Repr::Custom(crate::errors::Custom { kind, error }) => {
write!(f, "{kind}: {error}")
}
}
}
}
#[cfg(any(has_bundled_cli, test))]
impl std::fmt::Debug for EmbeddedCliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "EmbeddedCliError({self})")
}
}
#[cfg(any(has_bundled_cli, test))]
impl std::error::Error for EmbeddedCliError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.repr {
crate::errors::Repr::Custom(crate::errors::Custom { error, .. }) => Some(&**error),
_ => None,
}
}
}
#[cfg(test)]
mod tests;