дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(gradle): link and unlink independent Gradle builds (#1413)
Opening a folder of independent Gradle builds selected only the first build. Link Gradle Projects… (pick a build's settings or build file) and Unlink on a build's root let an ordinary project window manage several independent builds without changing their scripts. The build model holds a list of Gradle builds; each keeps its own wrapper, working directory, environment, task routing and sync cache, keyed by the owning build directory. The links persist in the project's Gradle settings (linkedProjects); unlinking leaves files and saved run configurations alone. Existing includeBuild support is unchanged, and an explicitly linked build takes precedence over a composite that also includes it. Workspace windows keep Link/Unlink disabled. Co-authored-by: gudcks0305 <[email protected]>
Цей коміт міститься в:
1 джерело
682fee633d
коміт
0dfdbbc323
43 files changed
+1846
-254
No files matched your search
@@ -50,7 +50,8 @@ async function ensureConfigs() {
|
||||
export function projectContentRoots(): string[] {
|
||||
const own = validateProjectStructure(useProject.getState().settings?.projectStructure).contentRoots;
|
||||
// The build model keeps the folders of modules outside the project absolute.
|
||||
const gradle = (useBuildModel.getState().model?.gradle?.projects ?? [])
|
||||
const gradle = (useBuildModel.getState().model?.gradle ?? [])
|
||||
.flatMap((g) => g.projects)
|
||||
.map((p) => p.dir)
|
||||
.filter((d) => isAbsolute(d));
|
||||
return [...new Set([...own, ...gradle])];
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { GradleModel, RunConfig } from "@/platform/ipc";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { GradleModel, ProjectInfo, RunConfig } from "@/platform/ipc";
|
||||
import { applicationConfig } from "./application";
|
||||
import { DELEGATED_DEBUG, delegatedGradleConfig, withTestEvents, wrapperGradleVersion } from "./gradleRun";
|
||||
import {
|
||||
DELEGATED_DEBUG,
|
||||
delegatedGradleConfig,
|
||||
gradleDelegation,
|
||||
withTestEvents,
|
||||
wrapperGradleVersion,
|
||||
} from "./gradleRun";
|
||||
import { jdwpLaunch } from "./javaDebug";
|
||||
import { isJvmApplication } from "@/platform/javaLaunch";
|
||||
import { useBuildModel } from "@/platform/buildModel";
|
||||
import { useProject } from "@/platform/project";
|
||||
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return {
|
||||
...real,
|
||||
isTauri: () => true,
|
||||
call: (value: unknown) => value,
|
||||
commands: {
|
||||
...real.commands,
|
||||
runGradleInitScript: () => Promise.resolve("/tmp/openidea-gradle-run.gradle"),
|
||||
fsReadText: () => Promise.resolve("distributionUrl=https://x/gradle-8.10-bin.zip"),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ROOT = "/w/shop";
|
||||
const CORE = "/w/billing/modules/core";
|
||||
@@ -143,3 +165,54 @@ describe("Gradle test runs", () => {
|
||||
expect(withTestEvents(twice, script)).toBe(twice);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delegation across independent builds", () => {
|
||||
afterEach(() => {
|
||||
useProject.setState({ project: null, settings: null });
|
||||
useBuildModel.setState({ root: null, model: null, gradleOffline: false });
|
||||
});
|
||||
|
||||
it("selects the module's linked build and preserves composite identity task paths", async () => {
|
||||
const builds = ["first", "second"].map((dir) => ({
|
||||
...gradle,
|
||||
dir,
|
||||
command: `${ROOT}/${dir}/gradlew`,
|
||||
env: { JAVA_HOME: `/${dir}/jdk` },
|
||||
projects: [{ path: ":app", name: "app", dir: `${dir}/app`, buildFile: null, tasks: [] }],
|
||||
}));
|
||||
useProject.setState({ project: { root: ROOT } as ProjectInfo, settings: null });
|
||||
useBuildModel.setState({ root: ROOT, model: { maven: null, gradle: builds }, gradleOffline: true });
|
||||
const app = {
|
||||
...applicationConfig({ className: "a.Main" }, "second/app/src/main/java/a/Main.java", []),
|
||||
module: "second/app",
|
||||
};
|
||||
const run = await gradleDelegation(app);
|
||||
expect(run).toMatchObject({ command: `${ROOT}/second/gradlew`, cwd: "second", env: { JAVA_HOME: "/second/jdk" } });
|
||||
expect(run!.args).toContain("--offline");
|
||||
expect(run!.args.at(-1)).toBe(":app:openideaRun");
|
||||
});
|
||||
|
||||
it("prefers the directly linked owner over a longer sibling composite root", async () => {
|
||||
const composite = {
|
||||
...gradle,
|
||||
dir: "very-long-composite",
|
||||
command: `${ROOT}/very-long-composite/gradlew`,
|
||||
projects: [{ path: ":b:app", name: "app", dir: "b/app", buildFile: null, tasks: [], build: ":b" }],
|
||||
};
|
||||
const direct = {
|
||||
...gradle,
|
||||
dir: "b",
|
||||
command: `${ROOT}/b/gradlew`,
|
||||
projects: [{ path: ":app", name: "app", dir: "b/app", buildFile: null, tasks: [] }],
|
||||
};
|
||||
useProject.setState({ project: { root: ROOT } as ProjectInfo, settings: null });
|
||||
useBuildModel.setState({ root: ROOT, model: { maven: null, gradle: [composite, direct] } });
|
||||
const app = {
|
||||
...applicationConfig({ className: "a.Main" }, "b/app/src/main/java/a/Main.java", []),
|
||||
module: "b/app",
|
||||
};
|
||||
const run = await gradleDelegation(app);
|
||||
expect(run).toMatchObject({ command: `${ROOT}/b/gradlew`, cwd: "b" });
|
||||
expect(run!.args.at(-1)).toBe(":app:openideaRun");
|
||||
});
|
||||
});
|
||||
@@ -135,12 +135,23 @@ export async function gradleDelegation(config: RunConfig): Promise<RunConfig | n
|
||||
const { project, settings } = useProject.getState();
|
||||
if (!project?.root || settings?.languages?.java?.gradle?.runUsing === "ide") return null;
|
||||
const { model, gradleOffline } = useBuildModel.getState();
|
||||
if (!model?.gradle) return null;
|
||||
if (!model?.gradle?.length) return null;
|
||||
const module = config.module ?? (await findJavaModule(config));
|
||||
if (module === null) return null;
|
||||
const dir = absolute(project.root, module);
|
||||
// A composite can report a sibling's module; prefer a linked root that contains it.
|
||||
const ownerDepth = (g: GradleModel) => {
|
||||
const root = absolute(project.root, g.dir).replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
const path = dir.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
return path === root || path.startsWith(`${root}/`) ? root.split("/").length : 0;
|
||||
};
|
||||
const gradle = model.gradle
|
||||
.filter((g) => g.projects.some((p) => absolute(project.root, p.dir) === dir))
|
||||
.sort((a, b) => ownerDepth(b) - ownerDepth(a))[0];
|
||||
if (!gradle) return null;
|
||||
const ctx = {
|
||||
root: project.root,
|
||||
gradle: model.gradle,
|
||||
gradle,
|
||||
script: "",
|
||||
offline: gradleOffline,
|
||||
configurationCache: false,
|
||||
|
||||
Reference in new issue
Заблокувати користувача