дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-11 20:25:41 +02:00
Порівняти коміти
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
606aff1613 |
No files matched your search
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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/*");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача