mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-13 20:13:56 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
606aff1613 |
No files matched your search
@@ -1,13 +1,14 @@
|
|||||||
//! Deployment over SFTP or local folders, and SSH run targets.
|
//! Deployment over SFTP or local folders, and SSH run targets.
|
||||||
//! Shared by IPC commands and project save/delete hooks.
|
//! Shared by IPC commands and project save/delete hooks.
|
||||||
|
|
||||||
use crate::deploy::{Deployment, SyncEntry, SyncStep, TransferReport};
|
use crate::deploy::{Deployment, FileStamp, SyncEntry, SyncStatus, SyncStep, TransferReport, status};
|
||||||
use crate::mapping::{Excludes, Mappings, ResolvedMapping};
|
use crate::mapping::{Excludes, Mappings, ResolvedMapping, posix};
|
||||||
use crate::target::{RemoteRuns, RemoteStart, SyncFn};
|
use crate::target::{RemoteRuns, RemoteStart, SyncFn, rewrite_external_classpath};
|
||||||
use ide_core::remote_settings::{AutoUpload, DeploymentServer, DeploymentType, RunTarget};
|
use ide_core::remote_settings::{AutoUpload, DeploymentServer, DeploymentType, RunTarget};
|
||||||
use ide_core::settings::Settings;
|
use ide_core::settings::Settings;
|
||||||
use ide_core::{CancellationToken, IdeError, IdeResult, SharedSink};
|
use ide_core::{CancellationToken, IdeError, IdeResult, SharedSink};
|
||||||
use ide_run::{OutputChunk, RunConfig, RunInfo};
|
use ide_run::{OutputChunk, RunConfig, RunInfo};
|
||||||
|
use ide_ssh::sftp::{join, mtime_of};
|
||||||
use ide_ssh::{FsConnection, RemoteEntry, RemoteFs, SshManager};
|
use ide_ssh::{FsConnection, RemoteEntry, RemoteFs, SshManager};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -359,10 +360,29 @@ impl RemoteService {
|
|||||||
return Err(e);
|
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 sync: Option<SyncFn> = if target.sync_before_run {
|
||||||
let deployment = settings.deployment.clone();
|
let deployment = settings.deployment.clone();
|
||||||
let root = root.to_path_buf();
|
let root = root.to_path_buf();
|
||||||
let remote_root = remote_root.clone();
|
let remote_root = remote_root.clone();
|
||||||
|
let preserve_time = settings.deployment.preserve_timestamps;
|
||||||
Some(Box::new(move |out| {
|
Some(Box::new(move |out| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let server = DeploymentServer { root_path: remote_root.clone(), ..Default::default() };
|
let server = DeploymentServer { root_path: remote_root.clone(), ..Default::default() };
|
||||||
@@ -377,6 +397,7 @@ impl RemoteService {
|
|||||||
};
|
};
|
||||||
let mut progress = |_: &str, _: usize, _: usize| {};
|
let mut progress = |_: &str, _: usize, _: usize| {};
|
||||||
let report = d.push_changes(&root, &mut progress).await;
|
let report = d.push_changes(&root, &mut progress).await;
|
||||||
|
let synced = sync_external_libraries(&fs, &libraries, preserve_time).await;
|
||||||
fs.close().await;
|
fs.close().await;
|
||||||
let report = report?;
|
let report = report?;
|
||||||
out(OutputChunk::System {
|
out(OutputChunk::System {
|
||||||
@@ -387,7 +408,17 @@ impl RemoteService {
|
|||||||
ide_core::error::ErrorKind::Io,
|
ide_core::error::ErrorKind::Io,
|
||||||
format!("upload of {} failed: {}", e.path, e.message),
|
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
|
None
|
||||||
};
|
};
|
||||||
let start = RemoteStart { session, target, root: root.to_path_buf(), remote_root, sync };
|
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 {
|
fn sanitize(s: &str) -> String {
|
||||||
s.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect()
|
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 parking_lot::Mutex;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
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)
|
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(
|
fn target_command(
|
||||||
config: &RunConfig,
|
config: &RunConfig,
|
||||||
target: &RunTarget,
|
target: &RunTarget,
|
||||||
@@ -381,4 +480,51 @@ mod tests {
|
|||||||
assert_eq!(replace_root("/work/proj", "/work/proj", "/srv"), "/srv");
|
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");
|
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
Block a user