src/bootstrap/src/utils/helpers.rs RUST 565 lines View on github.com → Search inside
1//! Various utility functions used throughout bootstrap.2//!3//! Simple things like testing the various filesystem operations here and there,4//! not a lot of interesting happenings here unfortunately.56use std::ffi::OsStr;7use std::path::{Path, PathBuf};8use std::sync::OnceLock;9use std::thread::panicking;10use std::time::{Instant, SystemTime, UNIX_EPOCH};11use std::{env, fs, io, panic, str};1213use object::read::archive::ArchiveFile;1415use crate::BootstrapOverrideLld;16use crate::core::builder::Builder;17use crate::core::config::{Config, TargetSelection};18use crate::utils::exec::{BootstrapCommand, command};19pub use crate::utils::shared_helpers::{dylib_path, dylib_path_var};2021#[cfg(test)]22mod tests;2324/// A wrapper around `std::panic::Location` used to track the location of panics25/// triggered by `t` macro usage.26pub struct PanicTracker<'a>(pub &'a panic::Location<'a>);2728impl Drop for PanicTracker<'_> {29    fn drop(&mut self) {30        if panicking() {31            eprintln!(32                "Panic was initiated from {}:{}:{}",33                self.0.file(),34                self.0.line(),35                self.0.column()36            );37        }38    }39}4041/// A helper macro to `unwrap` a result except also print out details like:42///43/// * The file/line of the panic44/// * The expression that failed45/// * The error itself46///47/// This is currently used judiciously throughout the build system rather than48/// using a `Result` with `try!`, but this may change one day...49#[macro_export]50macro_rules! t {51    ($e:expr) => {{52        let _panic_guard = $crate::PanicTracker(std::panic::Location::caller());53        match $e {54            Ok(e) => e,55            Err(e) => panic!("{} failed with {}", stringify!($e), e),56        }57    }};58    // it can show extra info in the second parameter59    ($e:expr, $extra:expr) => {{60        let _panic_guard = $crate::PanicTracker(std::panic::Location::caller());61        match $e {62            Ok(e) => e,63            Err(e) => panic!("{} failed with {} ({:?})", stringify!($e), e, $extra),64        }65    }};66}6768pub use t;69pub fn exe(name: &str, target: TargetSelection) -> String {70    crate::utils::shared_helpers::exe(name, &target.triple)71}7273/// Returns the path to the split debug info for the specified file if it exists.74pub fn split_debuginfo(name: impl Into<PathBuf>) -> Option<PathBuf> {75    // FIXME: only msvc is currently supported7677    let path = name.into();78    let pdb = path.with_extension("pdb");79    if pdb.exists() {80        return Some(pdb);81    }8283    // pdbs get named with '-' replaced by '_'84    let file_name = pdb.file_name()?.to_str()?.replace("-", "_");8586    let pdb: PathBuf = [path.parent()?, Path::new(&file_name)].into_iter().collect();87    pdb.exists().then_some(pdb)88}8990/// Returns `true` if the file name given looks like a dynamic library.91pub fn is_dylib(path: &Path) -> bool {92    path.extension().and_then(|ext| ext.to_str()).is_some_and(|ext| {93        ext == "dylib" || ext == "so" || ext == "dll" || (ext == "a" && is_aix_shared_archive(path))94    })95}9697/// Return the path to the containing submodule if available.98pub fn submodule_path_of(builder: &Builder<'_>, path: &str) -> Option<String> {99    let submodule_paths = builder.submodule_paths();100    submodule_paths.iter().find_map(|submodule_path| {101        if path.starts_with(submodule_path) { Some(submodule_path.to_string()) } else { None }102    })103}104105fn is_aix_shared_archive(path: &Path) -> bool {106    let file = match fs::File::open(path) {107        Ok(file) => file,108        Err(_) => return false,109    };110    let reader = object::ReadCache::new(file);111    let archive = match ArchiveFile::parse(&reader) {112        Ok(result) => result,113        Err(_) => return false,114    };115116    archive117        .members()118        .filter_map(Result::ok)119        .any(|entry| String::from_utf8_lossy(entry.name()).contains(".so"))120}121122/// Returns `true` if the file name given looks like a debug info file123pub fn is_debug_info(name: &str) -> bool {124    // FIXME: consider split debug info on other platforms (e.g., Linux, macOS)125    name.ends_with(".pdb")126}127128/// Returns the corresponding relative library directory that the compiler's129/// dylibs will be found in.130pub fn libdir(target: TargetSelection) -> &'static str {131    if target.is_windows() || target.contains("cygwin") { "bin" } else { "lib" }132}133134/// Adds a list of lookup paths to `cmd`'s dynamic library lookup path.135/// If the dylib_path_var is already set for this cmd, the old value will be overwritten!136pub fn add_dylib_path(path: Vec<PathBuf>, cmd: &mut BootstrapCommand) {137    let mut list = dylib_path();138    for path in path {139        list.insert(0, path);140    }141    cmd.env(dylib_path_var(), t!(env::join_paths(list)));142}143144pub struct TimeIt(bool, Instant);145146/// Returns an RAII structure that prints out how long it took to drop.147pub fn timeit(builder: &Builder<'_>) -> TimeIt {148    TimeIt(builder.config.dry_run(), Instant::now())149}150151impl Drop for TimeIt {152    fn drop(&mut self) {153        let time = self.1.elapsed();154        if !self.0 {155            println!("\tfinished in {}.{:03} seconds", time.as_secs(), time.subsec_millis());156        }157    }158}159160/// Symlinks two directories, using junctions on Windows and normal symlinks on161/// Unix.162pub fn symlink_dir(config: &Config, original: &Path, link: &Path) -> io::Result<()> {163    if config.dry_run() {164        return Ok(());165    }166    let _ = fs::remove_dir_all(link);167    return symlink_dir_inner(original, link);168169    #[cfg(not(windows))]170    fn symlink_dir_inner(original: &Path, link: &Path) -> io::Result<()> {171        use std::os::unix::fs;172        fs::symlink(original, link)173    }174175    #[cfg(windows)]176    fn symlink_dir_inner(target: &Path, junction: &Path) -> io::Result<()> {177        junction::create(target, junction)178    }179}180181/// Return the host target on which we are currently running.182pub fn get_host_target() -> TargetSelection {183    TargetSelection::from_user(env!("BUILD_TRIPLE"))184}185186/// Rename a file if from and to are in the same filesystem or187/// copy and remove the file otherwise188pub fn move_file<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()> {189    match fs::rename(&from, &to) {190        Err(e) if e.kind() == io::ErrorKind::CrossesDevices => {191            std::fs::copy(&from, &to)?;192            std::fs::remove_file(&from)193        }194        r => r,195    }196}197198pub fn forcing_clang_based_tests() -> bool {199    if let Some(var) = env::var_os("RUSTBUILD_FORCE_CLANG_BASED_TESTS") {200        match &var.to_string_lossy().to_lowercase()[..] {201            "1" | "yes" | "on" => true,202            "0" | "no" | "off" => false,203            other => {204                // Let's make sure typos don't go unnoticed205                panic!(206                    "Unrecognized option '{other}' set in \207                        RUSTBUILD_FORCE_CLANG_BASED_TESTS"208                )209            }210        }211    } else {212        false213    }214}215216pub fn use_host_linker(target: TargetSelection) -> bool {217    // FIXME: this information should be gotten by checking the linker flavor218    // of the rustc target219    !(target.contains("emscripten")220        || target.contains("wasm32")221        || target.contains("nvptx")222        || target.contains("fortanix")223        || target.contains("fuchsia")224        || target.contains("bpf")225        || target.contains("switch"))226}227228pub fn target_supports_cranelift_backend(target: TargetSelection) -> bool {229    if target.contains("linux") {230        target.contains("x86_64")231            || target.contains("aarch64")232            || target.contains("s390x")233            || target.contains("riscv64gc")234    } else if target.contains("darwin") {235        target.contains("x86_64") || target.contains("aarch64")236    } else if target.is_windows() {237        target.contains("x86_64")238    } else {239        false240    }241}242243/// Value returned from [`is_valid_test_suite_arg`], which figures out which paths start with the244/// suite name (and therefore which should be run).245pub enum TestFilterCategory<'a> {246    /// If a path is equal to the name of the suite, this is returned.247    Fullsuite,248    /// If a path starts with the suite, the suite prefix is stripped and the rest is returned as249    /// this variant.250    Arg(&'a str),251    /// For paths that don't start with the suite.252    Uninteresting,253}254255pub fn is_valid_test_suite_arg<'a, P: AsRef<Path>>(256    path: &'a Path,257    suite_path: P,258    builder: &Builder<'_>,259) -> TestFilterCategory<'a> {260    let suite_path = suite_path.as_ref();261    let path = match path.strip_prefix(".") {262        Ok(p) => p,263        Err(_) => path,264    };265    if !path.starts_with(suite_path) {266        return TestFilterCategory::Uninteresting;267    }268    let abs_path = builder.src.join(path);269    let exists = abs_path.is_dir() || abs_path.is_file();270    if !exists {271        panic!(272            "Invalid test suite filter \"{}\": file or directory does not exist",273            abs_path.display()274        );275    }276    // Since test suite paths are themselves directories, if we don't277    // specify a directory or file, we'll get an empty string here278    // (the result of the test suite directory without its suite prefix).279    // Therefore, we need to filter these out, as only the first --test-args280    // flag is respected, so providing an empty --test-args conflicts with281    // any following it.282    match path.strip_prefix(suite_path).ok().and_then(|p| p.to_str()) {283        Some(s) if !s.is_empty() => TestFilterCategory::Arg(s),284        _ => TestFilterCategory::Fullsuite,285    }286}287288pub fn make(host: &str) -> PathBuf {289    if host.contains("dragonfly")290        || host.contains("freebsd")291        || host.contains("netbsd")292        || host.contains("openbsd")293    {294        PathBuf::from("gmake")295    } else {296        PathBuf::from("make")297    }298}299300/// Returns the last-modified time for `path`, or zero if it doesn't exist.301pub fn mtime(path: &Path) -> SystemTime {302    fs::metadata(path).and_then(|f| f.modified()).unwrap_or(UNIX_EPOCH)303}304305/// Returns `true` if `dst` is up to date given that the file or files in `src`306/// are used to generate it.307///308/// Uses last-modified time checks to verify this.309pub fn up_to_date(src: &Path, dst: &Path) -> bool {310    if !dst.exists() {311        return false;312    }313    let threshold = mtime(dst);314    let meta = match fs::metadata(src) {315        Ok(meta) => meta,316        Err(e) => panic!("source {src:?} failed to get metadata: {e}"),317    };318    if meta.is_dir() {319        dir_up_to_date(src, threshold)320    } else {321        meta.modified().unwrap_or(UNIX_EPOCH) <= threshold322    }323}324325/// Returns the filename without the hash prefix added by the cc crate.326///327/// Since v1.0.78 of the cc crate, object files are prefixed with a 16-character hash328/// to avoid filename collisions.329pub fn unhashed_basename(obj: &Path) -> &str {330    let basename = obj.file_stem().unwrap().to_str().expect("UTF-8 file name");331    basename.split_once('-').unwrap().1332}333334fn dir_up_to_date(src: &Path, threshold: SystemTime) -> bool {335    t!(fs::read_dir(src)).map(|e| t!(e)).all(|e| {336        let meta = t!(e.metadata());337        if meta.is_dir() {338            dir_up_to_date(&e.path(), threshold)339        } else {340            meta.modified().unwrap_or(UNIX_EPOCH) < threshold341        }342    })343}344345/// Adapted from <https://github.com/llvm/llvm-project/blob/782e91224601e461c019e0a4573bbccc6094fbcd/llvm/cmake/modules/HandleLLVMOptions.cmake#L1058-L1079>346///347/// When `clang-cl` is used with instrumentation, we need to add clang's runtime library resource348/// directory to the linker flags, otherwise there will be linker errors about the profiler runtime349/// missing. This function returns the path to that directory.350pub fn get_clang_cl_resource_dir(builder: &Builder<'_>, clang_cl_path: &str) -> PathBuf {351    // Similar to how LLVM does it, to find clang's library runtime directory:352    // - we ask `clang-cl` to locate the `clang_rt.builtins` lib.353    let mut builtins_locator = command(clang_cl_path);354    builtins_locator.args(["/clang:-print-libgcc-file-name", "/clang:--rtlib=compiler-rt"]);355356    let clang_rt_builtins = builtins_locator.run_capture_stdout(builder).stdout();357    let clang_rt_builtins = Path::new(clang_rt_builtins.trim());358    assert!(359        clang_rt_builtins.exists(),360        "`clang-cl` must correctly locate the library runtime directory"361    );362363    // - the profiler runtime will be located in the same directory as the builtins lib, like364    // `$LLVM_DISTRO_ROOT/lib/clang/$LLVM_VERSION/lib/windows`.365    let clang_rt_dir = clang_rt_builtins.parent().expect("The clang lib folder should exist");366    clang_rt_dir.to_path_buf()367}368369/// Returns a flag that configures LLD to use only a single thread.370/// If we use an external LLD, we need to find out which version is it to know which flag should we371/// pass to it (LLD older than version 10 had a different flag).372fn lld_flag_no_threads(373    builder: &Builder<'_>,374    bootstrap_override_lld: BootstrapOverrideLld,375    is_windows: bool,376) -> &'static str {377    static LLD_NO_THREADS: OnceLock<(&'static str, &'static str)> = OnceLock::new();378379    let new_flags = ("/threads:1", "--threads=1");380    let old_flags = ("/no-threads", "--no-threads");381382    let (windows_flag, other_flag) = LLD_NO_THREADS.get_or_init(|| {383        let newer_version = match bootstrap_override_lld {384            BootstrapOverrideLld::External => {385                let mut cmd = command("lld");386                cmd.arg("-flavor").arg("ld").arg("--version");387                let out = cmd.run_capture_stdout(builder).stdout();388                match (out.find(char::is_numeric), out.find('.')) {389                    (Some(b), Some(e)) => out.as_str()[b..e].parse::<i32>().ok().unwrap_or(14) > 10,390                    _ => true,391                }392            }393            _ => true,394        };395        if newer_version { new_flags } else { old_flags }396    });397    if is_windows { windows_flag } else { other_flag }398}399400pub fn dir_is_empty(dir: &Path) -> bool {401    t!(std::fs::read_dir(dir), dir).next().is_none()402}403404/// Extract the beta revision from the full version string.405///406/// The full version string looks like "a.b.c-beta.y". And we need to extract407/// the "y" part from the string.408pub fn extract_beta_rev(version: &str) -> Option<String> {409    let parts = version.splitn(2, "-beta.").collect::<Vec<_>>();410    parts.get(1).and_then(|s| s.find(' ').map(|p| s[..p].to_string()))411}412413pub enum LldThreads {414    Yes,415    No,416}417418/// Returns the linker arguments for rustc/rustdoc for the given builder and target.419pub fn linker_args(420    builder: &Builder<'_>,421    target: TargetSelection,422    lld_threads: LldThreads,423) -> Vec<String> {424    let mut args = linker_flags(builder, target, lld_threads);425426    if let Some(linker) = builder.linker(target) {427        args.push(format!("-Clinker={}", linker.display()));428    }429430    args431}432433/// Returns the linker arguments for rustc/rustdoc for the given builder and target, without the434/// -Clinker flag.435pub fn linker_flags(436    builder: &Builder<'_>,437    target: TargetSelection,438    lld_threads: LldThreads,439) -> Vec<String> {440    let mut args = vec![];441    if !builder.is_lld_direct_linker(target) && builder.config.bootstrap_override_lld.is_used() {442        match builder.config.bootstrap_override_lld {443            BootstrapOverrideLld::External => {444                args.push("-Clinker-features=+lld".to_string());445                args.push("-Clink-self-contained=-linker".to_string());446                args.push("-Zunstable-options".to_string());447            }448            BootstrapOverrideLld::SelfContained => {449                args.push("-Clinker-features=+lld".to_string());450                args.push("-Clink-self-contained=+linker".to_string());451                args.push("-Zunstable-options".to_string());452            }453            BootstrapOverrideLld::None => unreachable!(),454        };455456        if matches!(lld_threads, LldThreads::No) {457            args.push(format!(458                "-Clink-arg=-Wl,{}",459                lld_flag_no_threads(460                    builder,461                    builder.config.bootstrap_override_lld,462                    target.is_windows()463                )464            ));465        }466    }467    args468}469470pub fn add_rustdoc_cargo_linker_args(471    cmd: &mut BootstrapCommand,472    builder: &Builder<'_>,473    target: TargetSelection,474    lld_threads: LldThreads,475) {476    let args = linker_args(builder, target, lld_threads);477    let mut flags = cmd478        .get_envs()479        .find_map(|(k, v)| if k == OsStr::new("RUSTDOCFLAGS") { v } else { None })480        .unwrap_or_default()481        .to_os_string();482    for arg in args {483        if !flags.is_empty() {484            flags.push(" ");485        }486        flags.push(arg);487    }488    if !flags.is_empty() {489        cmd.env("RUSTDOCFLAGS", flags);490    }491}492493/// Converts `T` into a hexadecimal `String`.494pub fn hex_encode<T>(input: T) -> String495where496    T: AsRef<[u8]>,497{498    use std::fmt::Write;499500    input.as_ref().iter().fold(String::with_capacity(input.as_ref().len() * 2), |mut acc, &byte| {501        write!(&mut acc, "{byte:02x}").expect("Failed to write byte to the hex String.");502        acc503    })504}505506/// Create a `--check-cfg` argument invocation for a given name507/// and it's values.508pub fn check_cfg_arg(name: &str, values: Option<&[&str]>) -> String {509    // Creating a string of the values by concatenating each value:510    // ',values("tvos","watchos")' or '' (nothing) when there are no values.511    let next = match values {512        Some(values) => {513            let mut tmp = values.iter().flat_map(|val| [",", "\"", val, "\""]).collect::<String>();514515            tmp.insert_str(1, "values(");516            tmp.push(')');517            tmp518        }519        None => "".to_string(),520    };521    format!("--check-cfg=cfg({name}{next})")522}523524/// Prepares `BootstrapCommand` that runs git inside the source directory if given.525///526/// Whenever a git invocation is needed, this function should be preferred over527/// manually building a git `BootstrapCommand`. This approach allows us to manage528/// bootstrap-specific needs/hacks from a single source, rather than applying them on next to every529/// git command creation, which is painful to ensure that the required change is applied530/// on each one of them correctly.531#[track_caller]532pub fn git(source_dir: Option<&Path>) -> BootstrapCommand {533    let mut git = command("git");534    // git commands are almost always read-only, so cache them by default535    git.cached();536537    if let Some(source_dir) = source_dir {538        git.current_dir(source_dir);539        // If we are running inside git (e.g. via a hook), `GIT_DIR` is set and takes precedence540        // over the current dir. Un-set it to make the current dir matter.541        git.env_remove("GIT_DIR");542        // Also un-set some other variables, to be on the safe side (based on cargo's543        // `fetch_with_cli`). In particular un-setting `GIT_INDEX_FILE` is required to fix some odd544        // misbehavior.545        git.env_remove("GIT_WORK_TREE")546            .env_remove("GIT_INDEX_FILE")547            .env_remove("GIT_OBJECT_DIRECTORY")548            .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES");549    }550551    git552}553554/// Sets the file times for a given file at `path`.555pub fn set_file_times<P: AsRef<Path>>(path: P, times: fs::FileTimes) -> io::Result<()> {556    // Windows requires file to be writable to modify file times. But on Linux CI the file does not557    // need to be writable to modify file times and might be read-only.558    let f = if cfg!(windows) {559        fs::File::options().write(true).open(path)?560    } else {561        fs::File::open(path)?562    };563    f.set_times(times)564}

Code quality findings 18

Warning: Ignoring a Result or Option using 'let _ =' can hide errors or unexpected None values. Ensure the value is handled appropriately (match, if let, ?, expect) unless intentionally discarded with justification.
warning correctness discarded-result
let _ = fs::remove_dir_all(link);
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
match &var.to_string_lossy().to_lowercase()[..] {
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
/// Value returned from [`is_valid_test_suite_arg`], which figures out which paths start with the
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
let basename = obj.file_stem().unwrap().to_str().expect("UTF-8 file name");
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning correctness expect-usage
let basename = obj.file_stem().unwrap().to_str().expect("UTF-8 file name");
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
basename.split_once('-').unwrap().1
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning correctness expect-usage
let clang_rt_dir = clang_rt_builtins.parent().expect("The clang lib folder should exist");
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
(Some(b), Some(e)) => out.as_str()[b..e].parse::<i32>().ok().unwrap_or(14) > 10,
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
parts.get(1).and_then(|s| s.find(' ').map(|p| s[..p].to_string()))
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning correctness expect-usage
write!(&mut acc, "{byte:02x}").expect("Failed to write byte to the hex String.");
Info: This standard library function returns a Result. Ensure the Result is handled properly (e.g., using '?', match, if let) rather than potentially panicking with .unwrap() or .expect().
info correctness unhandled-result
let file = match fs::File::open(path) {
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info maintainability println-macro
println!("\tfinished in {}.{:03} seconds", time.as_secs(), time.subsec_millis());
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
match path.strip_prefix(suite_path).ok().and_then(|p| p.to_str()) {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
let newer_version = match bootstrap_override_lld {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info correctness match-wildcard
match (out.find(char::is_numeric), out.find('.')) {
Info: This standard library function returns a Result. Ensure the Result is handled properly (e.g., using '?', match, if let) rather than potentially panicking with .unwrap() or .expect().
info correctness unhandled-result
(Some(b), Some(e)) => out.as_str()[b..e].parse::<i32>().ok().unwrap_or(14) > 10,
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
flags.push(" ");
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
flags.push(arg);

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.