Порівняти коміти

...
Автор SHA1 Повідомлення Дата
arookieofc 606aff1613 fix(remote): sync outside-root classpath entries for SSH Java launches (#2641) 2026-11-11 14:58:49 +08:00
2 змінених файлів з 231 додано та 6 видалено

No files matched your search

+84 -5
Переглянути файл
@@ -1,13 +1,14 @@
//! Deployment over SFTP or local folders, and SSH run targets.
//! Shared by IPC commands and project save/delete hooks.
use crate::deploy::{Deployment, SyncEntry, SyncStep, TransferReport};
use crate::mapping::{Excludes, Mappings, ResolvedMapping};
use crate::target::{RemoteRuns, RemoteStart, SyncFn};
use crate::deploy::{Deployment, FileStamp, SyncEntry, SyncStatus, SyncStep, TransferReport, status};
use crate::mapping::{Excludes, Mappings, ResolvedMapping, posix};
use crate::target::{RemoteRuns, RemoteStart, SyncFn, rewrite_external_classpath};
use ide_core::remote_settings::{AutoUpload, DeploymentServer, DeploymentType, RunTarget};
use ide_core::settings::Settings;
use ide_core::{CancellationToken, IdeError, IdeResult, SharedSink};
use ide_run::{OutputChunk, RunConfig, RunInfo};
use ide_ssh::sftp::{join, mtime_of};
use ide_ssh::{FsConnection, RemoteEntry, RemoteFs, SshManager};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
@@ -359,10 +360,29 @@ impl RemoteService {
return Err(e);
}
};
// #2641: the resolved `-cp` may reference jars outside the project root
// (`~/.m2`), which deployment sync never uploads. Mirror them under the
// deployment and rewrite the command onto the remote copies. Entries inside
// the project root are untouched: `target_command` maps those already.
let (args, libraries) = rewrite_external_classpath(&config.args, root, &remote_root);
let mut config = config.clone();
config.args = args;
for (local, _) in &libraries {
if !local.exists() {
fs.close().await;
return Err(IdeError::invalid(format!(
"cannot run on '{}': classpath entry {} does not exist on this machine; \
package the application (for example `mvn package`) or fix the launch classpath",
target.name,
local.display()
)));
}
}
let sync: Option<SyncFn> = if target.sync_before_run {
let deployment = settings.deployment.clone();
let root = root.to_path_buf();
let remote_root = remote_root.clone();
let preserve_time = settings.deployment.preserve_timestamps;
Some(Box::new(move |out| {
Box::pin(async move {
let server = DeploymentServer { root_path: remote_root.clone(), ..Default::default() };
@@ -377,6 +397,7 @@ impl RemoteService {
};
let mut progress = |_: &str, _: usize, _: usize| {};
let report = d.push_changes(&root, &mut progress).await;
let synced = sync_external_libraries(&fs, &libraries, preserve_time).await;
fs.close().await;
let report = report?;
out(OutputChunk::System {
@@ -387,7 +408,17 @@ impl RemoteService {
ide_core::error::ErrorKind::Io,
format!("upload of {} failed: {}", e.path, e.message),
)),
None => Ok(()),
None => {
let synced = synced?;
if synced > 0 {
out(OutputChunk::System {
text: format!(
"Synced {synced} external classpath file(s) to {remote_root}/.remote-libs\n"
),
});
}
Ok(())
}
}
})
}))
@@ -396,10 +427,58 @@ impl RemoteService {
None
};
let start = RemoteStart { session, target, root: root.to_path_buf(), remote_root, sync };
Ok(self.runs.start(start, config, sink, on_output))
Ok(self.runs.start(start, &config, sink, on_output))
}
}
/// Uploads outside-root classpath entries to their mirrored remote copies,
/// skipping files the remote already has (same size and time). Returns the
/// uploaded count. Missing locals are rejected in `run` before the sync starts.
async fn sync_external_libraries(
fs: &RemoteFs,
libraries: &[(PathBuf, String)],
preserve_time: bool,
) -> IdeResult<usize> {
fn fail(path: &str, e: IdeError) -> IdeError {
IdeError::new(ide_core::error::ErrorKind::Io, format!("sync of {path} failed: {}", e.message))
}
let mut synced = 0;
for (local, remote_base) in libraries {
let mut files = Vec::new();
if local.is_dir() {
let walk = ignore::WalkBuilder::new(local).standard_filters(false).follow_links(false).build();
for entry in walk.flatten() {
let path = entry.path();
// `is_file` (not the entry type): symlinked jars count.
if !path.is_file() {
continue;
}
let Ok(rel) = path.strip_prefix(local) else { continue };
files.push((path.to_path_buf(), join(remote_base, &posix(rel))));
}
} else {
files.push((local.clone(), remote_base.clone()));
}
for (file, remote) in files {
let size = std::fs::metadata(&file).map_err(|e| fail(&remote, e.into()))?.len();
let local_stamp = FileStamp { size, mtime: mtime_of(&file) };
let remote_stamp = match fs.stat(&remote).await.map_err(|e| fail(&remote, e))? {
Some(e) if e.is_dir => {
return Err(IdeError::invalid(format!("cannot sync onto the folder {remote}")));
}
Some(e) => Some(FileStamp { size: e.size, mtime: e.mtime }),
None => None,
};
if matches!(status(Some(local_stamp), remote_stamp), SyncStatus::Same) {
continue;
}
fs.upload(&file, &remote, preserve_time).await.map_err(|e| fail(&remote, e))?;
synced += 1;
}
}
Ok(synced)
}
fn sanitize(s: &str) -> String {
s.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect()
}
+147 -1
Переглянути файл
@@ -10,7 +10,7 @@ use ide_ssh::{Output, RemoteProcess, Session, Sig};
use parking_lot::Mutex;
use std::collections::BTreeMap;
use std::future::Future;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -118,6 +118,105 @@ pub fn argv_command(config: &RunConfig, target: &RunTarget, root: &Path, remote_
target_command(config, target, root, remote_root, false)
}
/// `java` flags whose value is a path list that may point outside the project
/// (`~/.m2` jars, `--module-path` entries): deployment sync only uploads the
/// project root, so these entries need their own sync + rewrite (#2641).
const PATH_LIST_FLAGS: [&str; 6] =
["-cp", "-classpath", "--class-path", "-p", "--module-path", "--upgrade-module-path"];
/// The separator of a `-cp` value on this (client) machine.
pub fn classpath_separator() -> char {
if cfg!(windows) { ';' } else { ':' }
}
fn split_path_list(value: &str) -> Vec<&str> {
value.split(classpath_separator()).filter(|e| !e.is_empty()).collect()
}
/// Absolute classpath entries of `args` that live outside the project root.
/// Unresolved placeholders (`${…}`), wildcards and relative entries pass through:
///
/// callers map the project root already, and only concrete local files can be synced.
pub fn external_classpath_entries(args: &[String], root: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut i = 0;
while i < args.len() {
if PATH_LIST_FLAGS.contains(&args[i].as_str()) && i + 1 < args.len() {
let value = &args[i + 1];
if !value.contains("${") {
for entry in split_path_list(value) {
if entry.contains('*') {
continue;
}
let path = PathBuf::from(entry);
if path.is_absolute() && !path.starts_with(root) && !out.contains(&path) {
out.push(path);
}
}
}
i += 2;
} else {
i += 1;
}
}
out
}
/// The remote home of an outside-root dependency: mirrored under `.remote-libs`
/// (never `.openidea*`: deployment excludes those names), so the layout stays
/// debuggable and collision-free per client machine.
pub fn external_lib_remote(remote_root: &str, local: &Path) -> String {
let spelling = local.to_string_lossy().replace('\\', "/");
let stripped = spelling.trim_start_matches('/').replace(':', "");
join(remote_root.trim_end_matches('/'), &format!(".remote-libs/{stripped}"))
}
/// Rewrites the outside-root classpath entries of `args` onto their synced remote
/// copies; returns the new args and the (local entry, remote base) pairs to upload.
/// Entries inside the project root are untouched: `target_command` maps those.
pub fn rewrite_external_classpath(
args: &[String],
root: &Path,
remote_root: &str,
) -> (Vec<String>, Vec<(PathBuf, String)>) {
let mut table: Vec<(PathBuf, String)> = Vec::new();
let mut out = args.to_vec();
let mut i = 0;
while i < out.len() {
if PATH_LIST_FLAGS.contains(&out[i].as_str()) && i + 1 < out.len() {
let value = out[i + 1].clone();
if !value.contains("${") {
let mut rewritten = Vec::new();
let mut changed = false;
for entry in split_path_list(&value) {
let path = PathBuf::from(entry);
if !entry.contains('*') && path.is_absolute() && !path.starts_with(root) {
let remote = match table.iter().find(|(p, _)| p == &path) {
Some((_, r)) => r.clone(),
None => {
let r = external_lib_remote(remote_root, &path);
table.push((path, r.clone()));
r
}
};
rewritten.push(remote);
changed = true;
} else {
rewritten.push(entry.to_owned());
}
}
if changed {
out[i + 1] = rewritten.join(&classpath_separator().to_string());
}
}
i += 2;
} else {
i += 1;
}
}
(out, table)
}
fn target_command(
config: &RunConfig,
target: &RunTarget,
@@ -381,4 +480,51 @@ mod tests {
assert_eq!(replace_root("/work/proj", "/work/proj", "/srv"), "/srv");
assert_eq!(replace_root(r"C:\p\src/a.py C:\p2", r"C:\p", "/srv"), r"/srv/src/a.py C:\p2");
}
/// Unix/Windows spellings of the same layout: one inside-root entry, one
/// `~/.m2` jar twice, a placeholder value, a wildcard and a module path.
fn classpath_fixture() -> (PathBuf, Vec<String>, String, String) {
let (root, classes, m2) = if cfg!(windows) {
(r"C:\proj", r"C:\proj\target\classes", r"C:\Users\me\.m2\a.jar")
} else {
("/proj", "/proj/target/classes", "/home/me/.m2/a.jar")
};
let sep = classpath_separator();
let args = vec![
"-cp".to_owned(),
format!("{classes}{sep}{m2}{sep}{m2}"),
"-p".to_owned(),
m2.to_owned(),
"-cp".to_owned(),
"${classpath}".to_owned(),
"-cp".to_owned(),
"/elsewhere/lib/*".to_owned(),
];
(PathBuf::from(root), args, m2.to_owned(), classes.to_owned())
}
#[test]
fn collects_outside_root_classpath_entries_once() {
let (root, args, m2, _) = classpath_fixture();
let entries = external_classpath_entries(&args, &root);
assert_eq!(entries, vec![PathBuf::from(m2)]);
}
#[test]
fn rewrites_outside_root_entries_onto_remote_lib_copies() {
let (root, args, m2, classes) = classpath_fixture();
let (rewritten, table) = rewrite_external_classpath(&args, &root, "/srv/app");
let remote = if cfg!(windows) {
"/srv/app/.remote-libs/C/Users/me/.m2/a.jar".to_owned()
} else {
"/srv/app/.remote-libs/home/me/.m2/a.jar".to_owned()
};
assert_eq!(table, vec![(PathBuf::from(m2), remote.clone())]);
let sep = classpath_separator();
assert_eq!(rewritten[1], format!("{classes}{sep}{remote}{sep}{remote}"));
assert_eq!(rewritten[3], remote);
// Placeholders and wildcards pass through untouched.
assert_eq!(rewritten[5], "${classpath}");
assert_eq!(rewritten[7], "/elsewhere/lib/*");
}
}