дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(gradle): navigate to version catalogs imported from published artifacts (#1149)
A catalog declared in settings as create("name") { from("group:artifact:version") } is a published .toml Gradle downloads into its cache. Build-script accessors of such a catalog now go to and describe its entries like a local catalog's: gradle_published_catalog finds the file in Gradle's cache or the local Maven repository on the project's host. A catalog not downloaded yet is looked up again the next time the script opens.
Closes #1006
Цей коміт міститься в:
1 джерело
280c2da48f
коміт
1bc7fd06c1
7 files changed
+165
-18
No files matched your search
@@ -222,6 +222,22 @@ fn declaring_pom(
|
||||
)))
|
||||
}
|
||||
|
||||
/// The `.toml` of a version catalog the build imports as `group:artifact:version`, once a
|
||||
/// build has downloaded it (`ide_sdk::libraries::published_version_catalog`).
|
||||
pub async fn gradle_published_catalog(
|
||||
host: &Host,
|
||||
window: &str,
|
||||
coordinates: String,
|
||||
) -> IdeResult<Option<ide_core::IpcPath>> {
|
||||
let project = host.project(window)?;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
ide_sdk::libraries::published_version_catalog(&project.root, &project.settings.effective(), &coordinates)
|
||||
.map(Into::into)
|
||||
})
|
||||
.await
|
||||
.map_err(join_err)
|
||||
}
|
||||
|
||||
/// Where the direct dependency `dependency` of the Maven module `pom` is declared.
|
||||
pub async fn maven_dependency_declaration(
|
||||
host: &Host,
|
||||
@@ -348,6 +364,9 @@ pub(crate) fn register(d: &mut Dispatcher) {
|
||||
)
|
||||
.await
|
||||
});
|
||||
d.add("gradle_published_catalog", |c| async move {
|
||||
gradle_published_catalog(&c.host, &c.window, c.arg("coordinates")?).await
|
||||
});
|
||||
d.add("maven_dependency_declaration", |c| async move {
|
||||
maven_dependency_declaration(&c.host, &c.window, c.arg("pom")?, c.arg("dependency")?).await
|
||||
});
|
||||
|
||||
@@ -294,6 +294,36 @@ pub(crate) fn dependency_libraries(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The `.toml` of a version catalog published as `group:artifact:version` (Gradle's
|
||||
/// `version-catalog` plugin) that a build imports with `from("…")`: from Gradle's cache, else
|
||||
/// the local Maven repository. `None` until a build has downloaded it.
|
||||
pub fn published_version_catalog(root: &Path, s: &Settings, coordinates: &str) -> Option<PathBuf> {
|
||||
let mut parts = coordinates.trim().split(':');
|
||||
let d = BuildDependency {
|
||||
gradle: true,
|
||||
group: parts.next()?.to_owned(),
|
||||
artifact: parts.next()?.to_owned(),
|
||||
version: parts.next()?.to_owned(),
|
||||
};
|
||||
if parts.next().is_some() || !valid_coordinate(&d) {
|
||||
return None;
|
||||
}
|
||||
let file = format!("{}-{}.toml", d.artifact, d.version);
|
||||
let gradle = crate::apply::setting_path(root, &s.languages.java.gradle.user_home).or_else(gradle_user_home);
|
||||
let cached = gradle.and_then(|home| {
|
||||
let dir = home.join("caches/modules-2/files-2.1").join(&d.group).join(&d.artifact).join(&d.version);
|
||||
let mut found: Vec<PathBuf> =
|
||||
std::fs::read_dir(dir).ok()?.flatten().map(|e| e.path().join(&file)).filter(|p| p.is_file()).collect();
|
||||
found.sort();
|
||||
found.into_iter().next()
|
||||
});
|
||||
cached.or_else(|| {
|
||||
let repo = crate::apply::maven_local_repository(root, s).or_else(default_maven_repository)?;
|
||||
let path = repo.join(d.group.replace('.', "/")).join(&d.artifact).join(&d.version).join(&file);
|
||||
path.is_file().then_some(path)
|
||||
})
|
||||
}
|
||||
|
||||
/// No path separators, interpolations or `..` segments (coordinates become paths).
|
||||
fn valid_coordinate(d: &BuildDependency) -> bool {
|
||||
[&d.group, &d.artifact, &d.version]
|
||||
@@ -530,6 +560,26 @@ pub(crate) fn parse_go_modules(text: &str) -> Vec<(String, PathBuf)> {
|
||||
#[cfg(test)]
|
||||
mod go_cache_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn finds_published_version_catalogs_in_the_gradle_cache_and_maven_repository() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let mut s = Settings::default();
|
||||
s.languages.java.gradle.user_home = root.join("gradle").to_string_lossy().into_owned();
|
||||
s.languages.java.maven.local_repository = root.join("m2").to_string_lossy().into_owned();
|
||||
let cached = root.join("gradle/caches/modules-2/files-2.1/com.acme/catalog/1.2/abc123/catalog-1.2.toml");
|
||||
std::fs::create_dir_all(cached.parent().unwrap()).unwrap();
|
||||
std::fs::write(&cached, "[libraries]").unwrap();
|
||||
assert_eq!(published_version_catalog(root, &s, "com.acme:catalog:1.2"), Some(cached));
|
||||
let local = root.join("m2/com/acme/other/2.0/other-2.0.toml");
|
||||
std::fs::create_dir_all(local.parent().unwrap()).unwrap();
|
||||
std::fs::write(&local, "[libraries]").unwrap();
|
||||
assert_eq!(published_version_catalog(root, &s, " com.acme:other:2.0 "), Some(local));
|
||||
for missing in ["com.acme:catalog:9", "com.acme:catalog", "a:b:c:d", "../x:y:z"] {
|
||||
assert_eq!(published_version_catalog(root, &s, missing), None, "{missing}");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn vendor_roots_include_local_replacements_once() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -43,6 +43,18 @@ pub async fn build_sync(
|
||||
.await
|
||||
}
|
||||
|
||||
/// The `.toml` of a version catalog the build imports as `group:artifact:version`, once a
|
||||
/// build has downloaded it.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn gradle_published_catalog(
|
||||
window: Window,
|
||||
state: State<'_, AppState>,
|
||||
coordinates: String,
|
||||
) -> IdeResult<Option<ide_core::IpcPath>> {
|
||||
ide_host::ops::build::gradle_published_catalog(&state.host, window.label(), coordinates).await
|
||||
}
|
||||
|
||||
/// Where the direct dependency `dependency` of the Maven module `pom` is declared.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
|
||||
@@ -337,6 +337,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
run::run_test_reports,
|
||||
build::build_model,
|
||||
build::build_sync,
|
||||
build::gradle_published_catalog,
|
||||
build::maven_dependency_declaration,
|
||||
build::maven_exclude_dependency,
|
||||
build::build_run,
|
||||
|
||||
@@ -638,6 +638,11 @@ export const commands = {
|
||||
* Returns the refreshed model; a failed build is reported in its `syncError`.
|
||||
*/
|
||||
buildSync: (tool: BuildTool, request: RequestId, offline: boolean, profiles: string[], onProgress: Channel<SyncProgress>) => typedError<BuildModel, IdeError>(__TAURI_INVOKE("build_sync", { tool, request, offline, profiles, onProgress })),
|
||||
/**
|
||||
* The `.toml` of a version catalog the build imports as `group:artifact:version`, once a
|
||||
* build has downloaded it.
|
||||
*/
|
||||
gradlePublishedCatalog: (coordinates: string) => typedError<string | null, IdeError>(__TAURI_INVOKE("gradle_published_catalog", { coordinates })),
|
||||
/** Where the direct dependency `dependency` of the Maven module `pom` is declared. */
|
||||
mavenDependencyDeclaration: (pom: string, dependency: ArtifactKey) => typedError<Location, IdeError>(__TAURI_INVOKE("maven_dependency_declaration", { pom, dependency })),
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { accessorSegments, catalogEntry, chainAt, declaredCatalogs, parseCatalog } from "./versionCatalog";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const ipc = vi.hoisted(() => ({ fsReadText: vi.fn(), gradlePublishedCatalog: vi.fn() }));
|
||||
vi.mock("@/platform/ipc", () => ({
|
||||
call: async (p: Promise<{ status: string; data?: unknown; error?: unknown }>) => {
|
||||
const r = await p;
|
||||
if (r.status !== "ok") throw r.error;
|
||||
return r.data;
|
||||
},
|
||||
commands: ipc,
|
||||
onIdeEvent: () => () => {},
|
||||
}));
|
||||
|
||||
import {
|
||||
accessorSegments,
|
||||
catalogEntry,
|
||||
chainAt,
|
||||
declaredCatalogs,
|
||||
parseCatalog,
|
||||
readCatalogs,
|
||||
} from "./versionCatalog";
|
||||
|
||||
const toml = `[versions]
|
||||
springBoot = "3.5.16"
|
||||
@@ -64,11 +83,35 @@ describe("Gradle version catalogs", () => {
|
||||
create('tools') {
|
||||
from(files('gradle/tools.versions.toml'))
|
||||
}
|
||||
create("platformLibs") { from("com.acme:platform-catalog:1.4.0") }
|
||||
}
|
||||
}`;
|
||||
expect(declaredCatalogs(settings)).toEqual([
|
||||
{ name: "commonLibs", file: "../shared/common.versions.toml" },
|
||||
{ name: "tools", file: "gradle/tools.versions.toml" },
|
||||
{ name: "platformLibs", artifact: "com.acme:platform-catalog:1.4.0" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads a published catalog from where a build downloaded it", async () => {
|
||||
const files: Record<string, string> = {
|
||||
"/p/settings.gradle.kts": `dependencyResolutionManagement { versionCatalogs {
|
||||
create("platformLibs") { from("com.acme:platform-catalog:1.4.0") }
|
||||
create("later") { from("com.acme:later:1.0") }
|
||||
} }`,
|
||||
"/cache/platform-catalog-1.4.0.toml": '[libraries]\nspring-core = "org.springframework:spring-core:6.2.0"\n',
|
||||
};
|
||||
ipc.fsReadText.mockImplementation(async (path: string) =>
|
||||
path in files ? { status: "ok", data: files[path] } : { status: "error", error: { kind: "notFound" } },
|
||||
);
|
||||
ipc.gradlePublishedCatalog.mockImplementation(async (coordinates: string) => ({
|
||||
status: "ok",
|
||||
data: coordinates === "com.acme:platform-catalog:1.4.0" ? "/cache/platform-catalog-1.4.0.toml" : null,
|
||||
}));
|
||||
const { catalogs, complete } = await readCatalogs("/p/build.gradle.kts", "/p");
|
||||
expect(catalogs.map((c) => [c.name, c.path])).toEqual([["platformLibs", "/cache/platform-catalog-1.4.0.toml"]]);
|
||||
expect(catalogEntry(catalogs[0]!, ["spring", "core"])?.value).toBe('"org.springframework:spring-core:6.2.0"');
|
||||
// `later` isn't downloaded yet: read again next time.
|
||||
expect(complete).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -99,19 +99,29 @@ export function chainAt(line: string, pos: number): { from: number; to: number;
|
||||
return segments.length > 1 ? { from, to, segments } : null;
|
||||
}
|
||||
|
||||
/** Catalogs declared in settings: `create("name") { from(files("path")) }`, paths relative to the build root. */
|
||||
export function declaredCatalogs(settings: string): Array<{ name: string; file: string }> {
|
||||
const out: Array<{ name: string; file: string }> = [];
|
||||
const re = /create\(\s*["'](\w+)["']\s*\)\s*\{[^}]*?from\(\s*files\(\s*["']([^"']+)["']/g;
|
||||
for (let m = re.exec(settings); m; m = re.exec(settings)) out.push({ name: m[1]!, file: m[2]! });
|
||||
export type DeclaredCatalog = { name: string; file: string } | { name: string; artifact: string };
|
||||
|
||||
/**
|
||||
* Catalogs declared in settings: `create("name") { from(files("path")) }`, paths relative to the
|
||||
* build root, or `create("name") { from("group:artifact:version") }`, a published catalog.
|
||||
*/
|
||||
export function declaredCatalogs(settings: string): DeclaredCatalog[] {
|
||||
const out: DeclaredCatalog[] = [];
|
||||
const re =
|
||||
/create\(\s*["'](\w+)["']\s*\)\s*\{[^}]*?from\(\s*(?:files\(\s*["']([^"']+)["']|["']([^"':]+:[^"':]+:[^"']+)["'])/g;
|
||||
for (let m = re.exec(settings); m; m = re.exec(settings))
|
||||
out.push(m[2] !== undefined ? { name: m[1]!, file: m[2] } : { name: m[1]!, artifact: m[3]! });
|
||||
return out;
|
||||
}
|
||||
|
||||
const readText = (path: string) => call(commands.fsReadText(path)).catch(() => null);
|
||||
const join = (dir: string, rel: string) => (rel.startsWith("/") ? rel : `${dir}/${rel.replace(/^\.\//, "")}`);
|
||||
|
||||
/** The catalogs a build script sees: those of the nearest enclosing build (its settings file). */
|
||||
async function readCatalogs(script: string, root: string): Promise<Catalog[]> {
|
||||
/**
|
||||
* The catalogs a build script sees: those of the nearest enclosing build (its settings file).
|
||||
* `complete` is false while a published catalog isn't downloaded yet.
|
||||
*/
|
||||
export async function readCatalogs(script: string, root: string): Promise<{ catalogs: Catalog[]; complete: boolean }> {
|
||||
let dir = dirname(script);
|
||||
let settings: string | null = null;
|
||||
for (;;) {
|
||||
@@ -123,13 +133,17 @@ async function readCatalogs(script: string, root: string): Promise<Catalog[]> {
|
||||
const declared = declaredCatalogs(settings ?? "");
|
||||
if (!declared.some((d) => d.name === "libs")) declared.push({ name: "libs", file: "gradle/libs.versions.toml" });
|
||||
const catalogs = await Promise.all(
|
||||
declared.map(async ({ name, file }) => {
|
||||
const path = join(buildRoot, file);
|
||||
const text = await readText(path);
|
||||
return text === null ? null : parseCatalog(name, path, text);
|
||||
declared.map(async (d) => {
|
||||
const path =
|
||||
"file" in d
|
||||
? join(buildRoot, d.file)
|
||||
: await call(commands.gradlePublishedCatalog(d.artifact)).catch(() => null);
|
||||
const text = path ? await readText(path) : null;
|
||||
return text === null ? null : parseCatalog(d.name, path!, text);
|
||||
}),
|
||||
);
|
||||
return catalogs.filter((c): c is Catalog => c !== null);
|
||||
const complete = declared.every((d, i) => "file" in d || catalogs[i] !== null);
|
||||
return { catalogs: catalogs.filter((c): c is Catalog => c !== null), complete };
|
||||
}
|
||||
|
||||
const loaded = new Map<string, Catalog[]>();
|
||||
@@ -148,11 +162,14 @@ function load(script: string): void {
|
||||
pending.clear();
|
||||
scripts.forEach(load);
|
||||
});
|
||||
const p = readCatalogs(script, root);
|
||||
pending.set(script, p);
|
||||
void p.then((c) => {
|
||||
if (pending.get(script) === p) loaded.set(script, c);
|
||||
const p = readCatalogs(script, root).then(({ catalogs, complete }) => {
|
||||
if (pending.get(script) !== p) return catalogs;
|
||||
loaded.set(script, catalogs);
|
||||
// Read again when the script is next opened, once a build has downloaded the catalog.
|
||||
if (!complete) pending.delete(script);
|
||||
return catalogs;
|
||||
});
|
||||
pending.set(script, p);
|
||||
}
|
||||
|
||||
function open(entry: CatalogEntry, path: string) {
|
||||
|
||||
Reference in new issue
Заблокувати користувача