diff --git a/docs/wiki/build-system/00-start-here.md b/docs/wiki/build-system/00-start-here.md new file mode 100644 index 0000000..945eb30 --- /dev/null +++ b/docs/wiki/build-system/00-start-here.md @@ -0,0 +1,37 @@ +# Start here + +This wiki teaches you how the build system for the **Rimworld Development Environment** Rider plugin works, end-to-end, from zero. By the end, you should be able to clone the repo, build it, run it, modify it, and confidently bump versions of Gradle / IntelliJ Platform / rdgen / Rider SDK / Kotlin / JDK without comparing against unrelated JetBrains template repos. + +## What this is + +A graduated learning journey, not a reference dump. Pages assume only what came before them. Read top to bottom on first pass; bookmark §18 (recipes) and §07 (version-pinning map) for daily use. + +## Reading paths + +| You are... | Read | +|---|---| +| New to JetBrains plugins **and** Gradle | All of it, in order | +| Comfortable with Gradle, new to JetBrains plugins | Skip §03–§04, start at §05 | +| Comfortable with IntelliJ plugins, new to Rider plugins | Skim §01–§02, focus on §10–§14 | +| Coming back to do one task | §07 + §18 + the relevant §19 runbook | + +## What's in each part + +- **Part 1 — Foundation** (§01–§05). Conceptual teaching only. What a Rider plugin is, the two-tier mental model, just-enough-Gradle, the cast of build tools. +- **Part 2 — This project** (§06–§17). Tied to actual file:line in this repo. Repo tour, version map, annotated `build.gradle.kts`, the `:protocol` subproject, the .NET side, dual-csproj pattern, the `riderModel` bridge, the `prepareSandbox` glue, runIde, CI/publish, and a quirks ledger. +- **Part 3 — Operate** (§18–§19). Day-to-day recipes and version-bump runbooks. +- **Part 4 — Reference** (§20–§24). Diagrams, a contributed-tasks table, a glossary, where to ask JetBrains for help, and a refactor backlog. + +## Promise + +Sections 1–3 plus the relevant recipe is sufficient for 80% of contributor tasks. The rest is for when something unusual happens or you want to upgrade the build itself. + +## Audience tags + +Most pages in Part 2 lead with one of: + +- **This works the same as IntelliJ plugins; the only twist is X** — for readers from the IntelliJ side +- **This is unique to Rider plugins** — for the JVM↔.NET bridging story +- **This is a custom workaround in this repo, not standard** — for the local hacks + +Watch for these tags. They tell you whether your existing intuition applies. diff --git a/docs/wiki/build-system/part-1-foundation/01-what-is-a-jetbrains-rider-plugin.md b/docs/wiki/build-system/part-1-foundation/01-what-is-a-jetbrains-rider-plugin.md new file mode 100644 index 0000000..435eb9b --- /dev/null +++ b/docs/wiki/build-system/part-1-foundation/01-what-is-a-jetbrains-rider-plugin.md @@ -0,0 +1,53 @@ +# 01 · What is a JetBrains Rider plugin? + +**[Foundation]** + +Before you can read this build, you have to know what it's building. A Rider plugin is, physically, a `.zip` file with a specific directory layout that Rider unpacks into a plugins directory and loads at startup. + +## The artifact + +When `./gradlew buildPlugin` finishes, you get a file like `output/rimworlddev-2025.1.10.zip`. Inside it: + +``` +rimworlddev/ +├── lib/ +│ ├── rimworlddev.jar ← compiled Kotlin (the "frontend" half) +│ └── (transitive JARs) +├── dotnet/ ← THE BIT THAT'S UNIQUE TO RIDER +│ ├── ReSharperPlugin.RimworldDev.dll (the "backend" half) +│ ├── ReSharperPlugin.RimworldDev.pdb +│ ├── 0Harmony.dll +│ ├── AsmResolver.dll +│ ├── (etc — runtime DLL deps) +└── ProjectTemplates/ ← templates for "New Rimworld Mod" project type +``` + +Rider, on startup, finds this folder and loads the JARs into its JVM and the DLLs into its .NET host. Both halves run, side by side, talking to each other. + +## What's unique to Rider (vs. a regular IntelliJ plugin) + +A standard IntelliJ plugin has only the `lib/` folder — JARs only. Rider plugins additionally have a `dotnet/` folder because **Rider is a dual-process IDE**: a JVM IDE on top, plus a separate .NET process underneath that handles all the C# language smarts. A Rider plugin frequently needs to extend both processes, so it ships a JVM half AND a .NET half, packaged together. + +Everything else in this build system flows from that single fact. Most of the "weird" parts of `build.gradle.kts` exist because Gradle has to: +1. Build the JVM half (it knows how) +2. Build the .NET half (it does NOT know how natively — has to shell out to `dotnet build`) +3. Generate the wire-protocol code so the two halves can talk +4. Glue both halves into the right folders inside that ZIP +5. Optionally launch a Rider IDE against the result so you can poke at it + +## One-paragraph mental model + +> **This plugin has two halves. The "frontend" is JVM/Kotlin and runs inside Rider's UI process. The "backend" is .NET/C# and runs inside Rider's ReSharper-host process — the same process that already understands C# code, NuGet, MSBuild, etc. The two halves talk over Rider's RPC pipe (called "RD" — rdgen for the generator, RdFramework for the runtime). Gradle is the conductor: it builds the JVM half itself, shells out to `dotnet build` for the .NET half, runs an `:protocol` subproject to produce matching Kotlin and C# wire-protocol stubs from a single Kotlin model, then "prepares the sandbox" by laying out a fake plugin directory containing both halves' artifacts plus their dependencies, and finally either zips it (`buildPlugin`), launches a real Rider against it (`runIde`), or uploads it to the Marketplace (`publishPlugin`).** + +That paragraph is the whole shape. Everything from here on is detail. + +## Where this plugin's source lives + +- **Frontend (Kotlin)**: `src/rider/main/kotlin/`, `src/rider/main/resources/META-INF/plugin.xml` +- **Backend (.NET/C#)**: `src/dotnet/ReSharperPlugin.RimworldDev/` +- **Protocol DSL (the wire format)**: `protocol/src/main/kotlin/model/rider/Model.kt` +- **Generated bindings (committed)**: `src/rider/main/kotlin/remodder/*.Generated.kt` and `src/dotnet/ReSharperPlugin.RimworldDev/*.Generated.cs` + +The non-default `src/rider/...` and `src/dotnet/...` paths exist because the repo holds two languages side by side. They're explicitly wired in `build.gradle.kts:66-72`. + +→ Next: [02 · The two-tier mental model](02-the-two-tier-mental-model.md) diff --git a/docs/wiki/build-system/part-1-foundation/02-the-two-tier-mental-model.md b/docs/wiki/build-system/part-1-foundation/02-the-two-tier-mental-model.md new file mode 100644 index 0000000..a4a16a3 --- /dev/null +++ b/docs/wiki/build-system/part-1-foundation/02-the-two-tier-mental-model.md @@ -0,0 +1,66 @@ +# 02 · The two-tier mental model + +**[Foundation]** + +Every confusing thing in this build is downstream of one fact: the running plugin spans two operating-system processes, each of which speaks a different language and has its own ecosystem. You have to internalize the boundary before the build's shape will make sense. + +## The two processes + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Rider IDE process (JVM, Kotlin/Java) │ +│ │ +│ - All UI: tool windows, dialogs, run config dialogs │ +│ - PSI for non-C# files (XML, JSON, etc.) │ +│ - Run configurations (Run/Debug) │ +│ - Settings UI │ +│ - Frontend half of this plugin: src/rider/main/kotlin/... │ +└──────────────────────────┬──────────────────────────────────┘ + │ + RD protocol over a pipe + (typed RPC, async + sync) + │ +┌──────────────────────────┴──────────────────────────────────┐ +│ Rider.Backend / ReSharper host process (.NET, C#) │ +│ │ +│ - C# language understanding (PSI, types, references) │ +│ - ReSharper analyzers, completions, quick-fixes │ +│ - MSBuild project model, NuGet awareness │ +│ - Decompiler │ +│ - Backend half of this plugin: src/dotnet/... │ +└─────────────────────────────────────────────────────────────┘ +``` + +When you see a UI (e.g. a tool window listing transpiled methods), the click flows through the JVM process. When you ask "where is this `def` defined?" by Ctrl-clicking inside Rimworld XML, the actual lookup runs in the .NET process — because it's the .NET process that has the C# type system loaded and can read `Assembly-CSharp.dll`. + +## Where does new code go? + +A decision matrix you'll consult often: + +| New feature | Where | Why | +|---|---|---| +| Tool window, dialog, settings page | Frontend (Kotlin) | UI is JVM-only | +| Run configuration | Frontend (Kotlin) | Run configs are an IntelliJ Platform concept | +| XML completion items derived from Rimworld's `Assembly-CSharp` | Backend (C#) | Needs the C# type system | +| ReSharper analyzer or quick-fix | Backend (C#) | Analyzers are a ReSharper concept | +| Decompilation, IL inspection | Backend (C#) | Uses .NET libraries (AsmResolver, ICSharpCode.Decompiler) | +| Anything that needs to call across | Both, plus an RD endpoint | The protocol is how they communicate | + +## How the halves talk + +JetBrains' answer is **RD (Reactive Distributed)** — a typed RPC system. You write a single Kotlin file (`protocol/src/main/kotlin/model/rider/Model.kt`) that declares calls, signals, and properties. A Gradle task (`:protocol:rdgen`) reads that and emits **two** files: a Kotlin one for the frontend and a C# one for the backend. Both files describe the same wire format, in their respective languages. At runtime the two sides bind to a shared pipe and method invocations are marshalled across. + +This plugin currently has exactly one RPC: `decompile(string[]) -> string[]`, defined at `protocol/src/main/kotlin/model/rider/Model.kt:16`. The frontend's Transpilation Explorer tool window calls it; the backend uses ICSharpCode.Decompiler to do the work. + +## Why the build feels weird because of this + +The build system is, at heart, a JVM build (Gradle) that has to: + +1. Build the .NET half by shelling out to `dotnet` +2. Coordinate a code generator (rdgen) that targets two languages +3. Stitch JARs and DLLs into a single ZIP with a layout the IDE expects +4. Launch a JVM IDE that itself launches a .NET process — both of which need to find the plugin's bits + +Gradle has no native support for any of this. The IntelliJ Platform Gradle Plugin (IPGP) handles a lot. The rest is custom glue inside `build.gradle.kts`. The "weird" tasks (`compileDotNet`, `prepareSandbox`'s manual DLL list, the `riderModel` configuration, the DotFiles patcher) all exist to plaster over this gap. + +→ Next: [03 · Gradle 101 — just enough](03-gradle-101-just-enough.md) diff --git a/docs/wiki/build-system/part-1-foundation/03-gradle-101-just-enough.md b/docs/wiki/build-system/part-1-foundation/03-gradle-101-just-enough.md new file mode 100644 index 0000000..e8d0d91 --- /dev/null +++ b/docs/wiki/build-system/part-1-foundation/03-gradle-101-just-enough.md @@ -0,0 +1,136 @@ +# 03 · Gradle 101 — just enough + +**[Foundation]** + +Just enough Gradle to read `build.gradle.kts` without panic. If you've used Maven but not Gradle, this page is for you. Skip if you've already built non-trivial Gradle projects. + +## The unit of work: a *task* + +Gradle is a graph of *tasks*. A task does one thing (compile some code, copy some files, zip a directory, run a test). Tasks have: + +- A **name** (e.g. `compileKotlin`) +- A **type** (e.g. `Exec`, `Copy`, `Jar`, or a custom one) +- **Inputs** and **outputs** (files Gradle tracks for incrementality) +- **`dependsOn`** edges to other tasks + +When you run `./gradlew runIde`, Gradle computes the closure of all tasks that need to run, orders them, and executes. + +## The three lifecycle phases (this is the secret one) + +This concept matters more than any other Gradle concept. Internalize it. + +1. **Initialization** — Gradle reads `settings.gradle.kts`, figures out what subprojects exist +2. **Configuration** — Gradle runs `build.gradle.kts` top to bottom, **including bodies of `tasks.foo { ... }` blocks**, just to register and configure tasks +3. **Execution** — Gradle runs the actions inside the chosen tasks (the `doLast { }` and `doFirst { }` blocks, or the built-in actions of typed tasks like `Exec`) + +```kotlin +val foo by tasks.registering(Exec::class) { + println("A") // runs at CONFIGURATION (every build, even if foo doesn't run) + executable("dotnet") + doLast { + println("B") // runs at EXECUTION (only when foo actually runs) + } +} +``` + +Things you'll do that depend on knowing this: +- Reading a file with `file(...).readText()` at the top of `build.gradle.kts` happens **at configuration time** — every build pays for it (e.g. `tasks.patchPluginXml { ... }` in this repo reads `CHANGELOG.md` eagerly; flagged for a later refactor) +- Wrapping work in `doLast { }` defers it to execution time + +## Tasks: register, named, configure + +Three syntaxes, all common: + +```kotlin +// Register a NEW task +val compileDotNet by tasks.registering(Exec::class) { + executable("dotnet") + args("build") +} + +// Configure an EXISTING task (added by a plugin) — same as `tasks.named("name") { ... }` +tasks.runIde { + dependsOn(compileDotNet) +} + +// Configure ALL tasks of a given type (now and in the future) +tasks.withType { + classpath(sourceSets["main"].runtimeClasspath) +} +``` + +When you see `tasks.runIde { ... }` in this codebase, you are *not* registering `runIde` — you're configuring one that the IntelliJ Platform Gradle Plugin already added. The block is a configuration action. + +## `dependsOn` is ordering, not data + +```kotlin +tasks.runIde { + dependsOn(compileDotNet) // "run compileDotNet before runIde" +} +``` + +`dependsOn` says "if you're going to run me, run that first." It does **not** declare any data flow. To get incremental builds, the producer task must declare `@OutputFiles` and the consumer must consume those as inputs. `dependsOn` alone doesn't mean "Gradle knows compileDotNet's output is fresh." + +This matters here: `compileDotNet` in this repo doesn't declare inputs/outputs (`build.gradle.kts:86-90`), so it always re-runs. Documented in §17 as a known issue, fixable later. + +## The `plugins { }` block + +Three shapes you'll see, all in `build.gradle.kts:6-11`: + +```kotlin +plugins { + id("java") // built-in + alias(libs.plugins.kotlinJvm) // version catalog reference + id("org.jetbrains.intellij.platform") version "2.14.0" // explicit version + id("me.filippov.gradle.jvm.wrapper") version "0.16.0" +} +``` + +`alias(libs.plugins.kotlinJvm)` resolves through `gradle/libs.versions.toml`'s `[plugins]` table — Gradle's "version catalog" feature. It's just a typed pointer to the same `id(...) version "..."` declaration; the value is centralized. + +## `by project` — the property delegate + +Throughout `build.gradle.kts:25-31` you'll see: + +```kotlin +val DotnetSolution: String by project +val PluginVersion: String by project +``` + +This is Kotlin property delegation. `by project` reads the property out of `gradle.properties` (or a CLI override like `-PPluginVersion=1.2.3`). The read is **eager** at configuration time — if the property is missing, the build fails the moment that line is evaluated. + +(Also possible: `by settings`, used in `settings.gradle.kts:4-14` because the settings script doesn't have a Project, only a Settings.) + +## `apply { plugin("...") }` — the older style + +```kotlin +apply { plugin("kotlin") } +``` + +Equivalent to declaring `id("kotlin")` in the `plugins { }` block. The newer form is preferred. This repo's `build.gradle.kts:53-55` does both, redundantly — flagged for cleanup in §17. + +## `extra` — Gradle's loose property bag + +```kotlin +val isWindows = Os.isFamily(Os.FAMILY_WINDOWS) +extra["isWindows"] = isWindows +``` + +A `Project`-attached map for ad-hoc properties readable across blocks. Used in `build.gradle.kts:22-23` to share OS detection so other blocks don't redo it. + +## What plugins contribute + +A Gradle plugin can: +- Register tasks (e.g. IPGP adds `prepareSandbox`, `runIde`, `buildPlugin`, `patchPluginXml`, `publishPlugin`, `verifyPlugin`) +- Add DSL extensions (e.g. `intellijPlatform { ... }`) +- Add repositories +- Add dependencies / configurations +- Wire `dependsOn` chains + +When you see `tasks.somethingYouNeverDefined { ... }` in `build.gradle.kts`, it's almost certainly contributed by a plugin. §21 has a full list of who contributes what. + +## A `Provider` is a lazy value + +You'll see `something.set(provider { ... })` and `argumentProviders += CommandLineArgumentProvider { ... }`. Gradle has been moving toward lazy/deferred values everywhere. The "set this value" idiom isn't `something = value` but `something.set(value)`. Covered in detail in §04. + +→ Next: [04 · Gradle 201 — providers and config cache](04-gradle-201-providers-and-config-cache.md) diff --git a/docs/wiki/build-system/part-1-foundation/04-gradle-201-providers-and-config-cache.md b/docs/wiki/build-system/part-1-foundation/04-gradle-201-providers-and-config-cache.md new file mode 100644 index 0000000..cbeb17b --- /dev/null +++ b/docs/wiki/build-system/part-1-foundation/04-gradle-201-providers-and-config-cache.md @@ -0,0 +1,128 @@ +# 04 · Gradle 201 — providers and config cache + +**[Foundation]** + +The half-step from "I can read tasks" to "I can edit them without breaking things." If you skip this, every `provider { }` and `Property` and `argumentProviders` reference will feel like incantation. + +## Why lazy values exist + +Gradle has two kinds of value APIs: eager and lazy. + +**Eager** (the old way): +```kotlin +val outputDir = file("build/output") // resolved NOW, at configuration time +``` + +**Lazy** (the modern way): +```kotlin +val outputDir = layout.buildDirectory.dir("output") // a Provider, resolved later +``` + +Why bother? Three reasons: + +1. **Configuration-time work shrinks** — eager file lookups, network calls, string interpolation all run on every build, even no-op ones. Lazy values defer that until the value is actually needed. +2. **The configuration cache** can serialize the task graph between runs. To do that, it has to know what's a "live" reference (forbidden in cached state) vs. a captured value (allowed). Providers are first-class to that machinery. +3. **Task-to-task wiring** — a downstream task can consume an upstream task's `Provider` without knowing whether the upstream task has actually run yet. Gradle resolves the chain at execution time. + +## `Property` and `.set(...)` + +The cliché Gradle DSL pattern: + +```kotlin +tasks.publishPlugin { + token.set(PublishToken) // NOT token = PublishToken +} + +tasks.patchPluginXml { + pluginVersion.set(PluginVersion) + untilBuild.set(provider { null }) +} +``` + +`token`, `pluginVersion`, `untilBuild` are all `Property` (a `Provider` that's also writable). You set them with `.set(...)`. The argument can be a literal value, another `Provider`, or a `provider { ... }` lambda. Lambdas re-evaluate at use-time. + +In this repo: +- `build.gradle.kts:235` — `token.set(PublishToken)` (`PublishToken` came from `by project`) +- `build.gradle.kts:249-251` — `pluginVersion.set(PluginVersion)`, `changeNotes.set(...)`, `untilBuild.set(provider { null })` (the `null` clears the auto-computed upper bound; see §17) + +## `provider { }` — the escape hatch + +When you have computation that should happen at use-time, not config-time: + +```kotlin +artifacts { + add(riderModel.name, provider { + intellijPlatform.platformPath.resolve("lib/rd/rider-model.jar").also { + check(it.isFile) { "..." } + } + }) { + builtBy(Constants.Tasks.INITIALIZE_INTELLIJ_PLATFORM_PLUGIN) + } +} +``` + +`build.gradle.kts:259-268`. The `provider { ... }` is necessary because `intellijPlatform.platformPath` doesn't exist until IPGP's initialize task has extracted the SDK. Resolving the path at configuration time would crash; resolving it inside `provider { ... }` defers until it's available. + +## `argumentProviders` — same idea, for command-line arguments + +```kotlin +tasks.runIde { + argumentProviders += CommandLineArgumentProvider { + listOf("${rootDir}/example-mod/AshAndDust.sln") + } +} +``` + +`build.gradle.kts:151-153`. `CommandLineArgumentProvider` is a SAM (single-method) interface. The lambda runs at execution time, not config time. Why use it for a static-looking string? Two reasons: idiomatic for IPGP, and config-cache-friendly. + +## The configuration cache + +Run `./gradlew :buildPlugin --configuration-cache` and Gradle will: + +1. On first run, serialize the entire task graph (configurations, task wiring, captured values) to disk +2. On subsequent runs with the same inputs, **skip the configuration phase entirely** and just execute + +This is huge for IDE plugin builds because configuration can take 5-15 seconds. The catch: certain things can't be cached. + +**Cache-hostile patterns** (don't do these): +- Reading `Project` inside `doLast` (`project.someProperty` in execution code) +- Reading `gradle`, `subprojects`, `tasks` inside execution-time code +- Reading files at config time and storing the contents (better: capture the file path as a `Provider`, read inside `doLast`) + +**Cache-friendly patterns**: +- Capturing `File` references in a `val` at config time, using them in `doLast` +- Wrapping reads in `provider { }` +- Using `Property` for task wiring + +In this repo, `build.gradle.kts:108-111` captures `pluginZip` and `outputDir` as `val`s at config time and uses `.get().asFile` inside `doLast`: + +```kotlin +val pluginZip = layout.buildDirectory.file("distributions/${rootProject.name}-${version}.zip") +val outputDir = layout.projectDirectory.dir("output").asFile +doLast { + val zipFile = pluginZip.get().asFile + outputDir.mkdirs() + // ... +} +``` + +This is the *correct* shape: file paths are determined at config time (fine, they're stable), file content / existence is checked at execution time (cache-safe). + +## Up-to-date checks (incrementality) + +Gradle decides whether to skip a task based on its declared inputs and outputs. A task with: + +```kotlin +@InputDirectory val src = "src/dotnet" +@OutputDirectory val out = "src/dotnet/.../bin" +``` + +…will skip if no input file changed since the last run. A task with no declared inputs/outputs will run every time. + +`compileDotNet`, `buildResharperPlugin`, and `testDotNet` in this repo are `Exec` tasks with no declared inputs/outputs (`build.gradle.kts:86-105`, `:226-230`). They always re-run. The `dependsOn(compileDotNet)` on `prepareSandbox` orders them, but doesn't make `compileDotNet` skippable. This is a known limitation, captured as a refactor opportunity in §24. + +## Why this matters before reading the build file + +When you encounter `something.set(provider { ... })`, `argumentProviders +=`, `val foo by tasks.registering(...)`, `extra["bar"] = ...`, or `pluginZip.get().asFile`, you'll know what shape of API you're looking at, why it's that shape, and what's safe to change. + +→ Next: [05 · The cast of tools](05-the-cast-of-tools.md) diff --git a/docs/wiki/build-system/part-1-foundation/05-the-cast-of-tools.md b/docs/wiki/build-system/part-1-foundation/05-the-cast-of-tools.md new file mode 100644 index 0000000..bd3e816 --- /dev/null +++ b/docs/wiki/build-system/part-1-foundation/05-the-cast-of-tools.md @@ -0,0 +1,51 @@ +# 05 · The cast of tools + +**[Foundation]** + +This build invokes about a dozen distinct tools. Here's a one-paragraph role for each. When the rest of the wiki name-drops one, come back here for the elevator pitch. + +## Build orchestration + +**Gradle** — the umbrella. Reads `build.gradle.kts` and `settings.gradle.kts`, builds the task graph, runs tasks in order. The wrapper script (`gradlew` / `gradlew.bat`) downloads the right Gradle version automatically; this repo pins Gradle 9.4.1 (`gradle/wrapper/gradle-wrapper.properties:3` and `build.gradle.kts:58`). + +**`me.filippov.gradle.jvm.wrapper`** — a Gradle plugin that bundles a JDK with the `gradlew` scripts so a fresh clone bootstraps even on a machine without JDK 21 installed. Probably removable now that Gradle has its own toolchain auto-provisioning (foojay-resolver-convention) — flagged in §24. + +## JVM / Kotlin side + +**Kotlin Gradle Plugin** — compiles Kotlin sources to JVM bytecode. Pulled in via `alias(libs.plugins.kotlinJvm)` (`build.gradle.kts:8`). Version pinned in `gradle/libs.versions.toml:2`. + +**IntelliJ Platform Gradle Plugin (IPGP) v2** — the giant. Knows how to download Rider from the JetBrains Maven repo, extract the SDK, register tasks like `prepareSandbox` / `runIde` / `buildPlugin` / `patchPluginXml` / `publishPlugin` / `verifyPlugin`, package the final ZIP in the right shape, and upload to the Marketplace. Roughly 70% of this build's "it just works" comes from IPGP. Plugin id: `org.jetbrains.intellij.platform`. Documented at . + +**JBR (JetBrains Runtime)** — JetBrains' fork of OpenJDK that ships with IntelliJ-family IDEs. The sandboxed Rider that `runIde` launches needs JBR specifically (not Corretto), because the bundled debugger and Mono integration assume JBR's instrumentation. Wired by `jetbrainsRuntime()` in `build.gradle.kts:49, :123`. + +## Cross-tier protocol + +**rdgen / RD framework** — JetBrains' "Reactive Distributed" RPC system. You write a single Kotlin DSL file declaring calls/signals/properties; rdgen produces matched Kotlin and C# bindings; the RdFramework runtime libraries (in both languages) marshal calls between the JVM and .NET halves. The `:protocol` subproject in this repo is dedicated to running rdgen. Lives at . + +## .NET / C# side + +**.NET SDK** — Microsoft's `dotnet` CLI. Builds the C# half. This repo pins .NET SDK 7.0.202 with `rollForward: latestMajor` in `global.json`. Gradle's `compileDotNet` task is just `dotnet build`. + +**MSBuild** — the build engine .NET uses, invoked through `dotnet msbuild` in the `buildResharperPlugin` task (`build.gradle.kts:92-105`). Reads `.csproj` files, applies `Directory.Build.props` automatically, runs targets like `Restore;Rebuild;Pack`. + +**`Directory.Build.props`** — an MSBuild file at the repo root that's auto-imported into every `.csproj`. Centralizes the .NET SDK version, NuGet package versions, output paths. Equivalent role to `gradle.properties` for the .NET side. + +**JetBrains.Rider.SDK / JetBrains.ReSharper.SDK** — NuGet packages providing the C# APIs to extend Rider's backend / ReSharper. Pinned to `$(SdkVersion)` in `Directory.Build.props:43, :40-41`. + +**Wave** — ReSharper's internal version stream. Wave 261 = ReSharper 2026.1, Wave 252 = 2025.2. The Wave NuGet package is referenced by the ReSharper-flavor csproj for ReSharper-for-VS compatibility; the version is computed from `SdkVersion` in `Directory.Build.props:33-35`. + +**AsmResolver, ICSharpCode.Decompiler, Lib.Harmony, Krafs.Publicizer** — third-party .NET libraries used by the "Remodder" feature (decompiling Rimworld's compiled code). Listed in `ReSharperPlugin.RimworldDev.Rider.csproj:27-35`. `prepareSandbox` manually copies the resulting DLLs into the plugin (`build.gradle.kts:160-171`). + +## Tools that are *legacy* in this repo + +**vswhere / nuget.exe / `tools/` directory** — bundled in the repo (`tools/vswhere.exe`, `tools/nuget.exe`) and called by `settings.ps1`. Used to find a Visual Studio install for the legacy ReSharper-for-VS local-dev flow via `runVisualStudio.ps1`. Not used by CI, not used by the Gradle build. The maintainer's stripping of vswhere from `compileDotNet` was a previous cleanup pass; the remnants survive in PowerShell scripts. + +**`buildPlugin.ps1`, `publishPlugin.ps1`, `settings.ps1`** — PowerShell scripts at repo root. Inherited from a JetBrains template. **Not invoked by CI.** They duplicate functionality already provided by Gradle (`buildResharperPlugin`, `publishPlugin`). Candidates for deletion (§24). Only `runVisualStudio.ps1` has a unique role (set up an experimental ReSharper hive in Visual Studio). + +## Glue you'll meet + +**`example-mod/`** — a real Rimworld mod checked into the repo. `runIde` opens it as a manual fixture so you have something to poke at when developing. Future home of integration tests (§17). + +**`.run/*.run.xml`** — IntelliJ run configurations checked into the repo. `Build Plugin.run.xml` runs `:buildPlugin -x compileDotNet`; `Build ReSharper Plugin.run.xml` invokes the legacy PowerShell. Both have stale references (`corretto-17.0.7`, the .ps1 path); §17 flags them. + +→ End of Part 1. Next: [06 · Repo tour](../part-2-this-project/06-repo-tour.md) diff --git a/docs/wiki/build-system/part-2-this-project/06-repo-tour.md b/docs/wiki/build-system/part-2-this-project/06-repo-tour.md new file mode 100644 index 0000000..3fc568d --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/06-repo-tour.md @@ -0,0 +1,98 @@ +# 06 · Repo tour + +**[This Project]** — *This is mostly the same as IntelliJ plugins, with a `src/dotnet/` tree added on the side.* + +A guided walk through the directory tree, top down. What's in each top-level entry, and why. + +``` +Rider-RimworldDevelopment/ +├── build.gradle.kts ← root Gradle build script +├── settings.gradle.kts ← Gradle settings (subprojects, pluginManagement) +├── gradle.properties ← simple key=value config +├── gradle/ +│ ├── libs.versions.toml ← version catalog (Kotlin, rdGen) +│ └── wrapper/ ← Gradle wrapper (pinned 9.4.1) +├── gradlew, gradlew.bat ← Gradle wrapper launcher +├── global.json ← .NET SDK pin (7.0.202) +├── Directory.Build.props ← MSBuild props for every .csproj +├── ReSharperPlugin.RimworldDev.sln ← .NET solution containing the 3 csprojs +│ +├── protocol/ ← :protocol subproject +│ ├── build.gradle.kts ← rdgen wiring +│ └── src/main/kotlin/model/rider/ +│ └── Model.kt ← THE protocol DSL (§10) +│ +├── src/ +│ ├── rider/main/ ← JVM/Kotlin frontend +│ │ ├── kotlin/ ← Kotlin sources +│ │ │ └── remodder/ +│ │ │ └── RemodderProtocolModel.Generated.kt ← rdgen output, COMMITTED +│ │ └── resources/ +│ │ └── META-INF/plugin.xml ← Rider plugin descriptor +│ │ +│ └── dotnet/ +│ ├── ReSharperPlugin.RimworldDev/ +│ │ ├── ReSharperPlugin.RimworldDev.csproj ← Wave/ReSharper flavor +│ │ ├── ReSharperPlugin.RimworldDev.Rider.csproj ← Rider flavor +│ │ ├── *.cs ← shared C# sources +│ │ ├── RemodderProtocolModel.Generated.cs ← rdgen output, COMMITTED +│ │ ├── ProjectTemplates/ ← New-Mod templates +│ │ ├── Remodder/ ← decompilation feature (Rider-only) +│ │ ├── References/ ← XML→C# navigation +│ │ ├── ItemCompletion/ ← XML autocompletion +│ │ ├── ProblemAnalyzers/ ← validation +│ │ ├── RimworldXmlProject/ ← custom project type +│ │ └── (other features) +│ └── ReSharperPlugin.RimworldDev.Tests/ +│ └── ReSharperPlugin.RimworldDev.Tests.csproj ← stub: zero .cs test files yet +│ +├── example-mod/ ← real Rimworld mod, opened by runIde +│ ├── AshAndDust.sln +│ ├── Source/ +│ ├── About/, Defs/, Patches/, Languages/, Textures/... +│ └── 1.4/, 1.5/, ... ← multi-version support folders +│ +├── .github/workflows/ +│ ├── CI.yml ← push/PR build & test +│ └── Deploy.yml ← tag-triggered publish +│ +├── .run/ ← IntelliJ run configurations (some stale) +│ ├── Build Plugin.run.xml +│ └── Build ReSharper Plugin.run.xml +│ +├── runVisualStudio.ps1 ← legitimate (legacy ReSharper-for-VS dev) +├── buildPlugin.ps1 ← LEGACY, not used by CI +├── publishPlugin.ps1 ← LEGACY, not used by CI +├── settings.ps1 ← LEGACY (vswhere wrapper) +├── tools/ +│ ├── vswhere.exe ← LEGACY +│ └── nuget.exe ← LEGACY +│ +├── CHANGELOG.md ← parsed at build time by patchPluginXml +├── README.md +└── output/ ← build artifacts land here (gitignored) + └── rimworlddev-X.Y.Z.zip ← the final Rider plugin distribution +``` + +## Notable conventions + +- **`src/rider/...` instead of `src/main/...`**: explicitly wired in `build.gradle.kts:66-72`. The repo holds two languages, so they're segregated under `rider/` (JVM) and `dotnet/` (.NET). A reader from a single-language Gradle project will hit this immediately and wonder why. +- **Generated files are committed**: both `RemodderProtocolModel.Generated.kt` and `RemodderProtocolModel.Generated.cs`. They're rdgen output. We commit them on purpose — see §10. +- **The .NET solution sits at the repo root** (`ReSharperPlugin.RimworldDev.sln`) but the projects live under `src/dotnet/`. That's an MSBuild convention quirk; the solution file references projects by relative path. +- **Three `.csproj` files share the source tree** at `src/dotnet/ReSharperPlugin.RimworldDev/`. They produce different assemblies via different `` and `` rules. See §12. +- **`example-mod/` is a real mod, not a synthetic test fixture** — passed to `runIde` as an argument so the launched Rider opens it. Currently a manual fixture; future basis for integration tests (§17). +- **`output/`** is where `tasks.buildPlugin { doLast { copy(...) } }` (`build.gradle.kts:107-114`) places the final ZIP. CI's GitHub Release upload reads from here, which is why the copy exists. + +## Files most likely to drift / mislead + +These are tagged in §17 with details: + +| File | Issue | +|---|---| +| `gradle.properties:26-28` vs `build.gradle.kts:9-10` | Same plugin versions, drifted (`gradleJvmWrapperVersion=0.15.0` vs `0.16.0`) | +| `gradle.properties:28` `riderBaseVersion` | Dead — zero references | +| `.run/Build Plugin.run.xml:8` | References `corretto-17.0.7` but toolchain is JDK 21 | +| `.run/Build ReSharper Plugin.run.xml` | Invokes legacy `buildPlugin.ps1` | +| `*.ps1` (root, except `runVisualStudio.ps1`) | Legacy; CI uses Gradle | + +→ Next: [07 · Version-pinning map](07-version-pinning-map.md) diff --git a/docs/wiki/build-system/part-2-this-project/07-version-pinning-map.md b/docs/wiki/build-system/part-2-this-project/07-version-pinning-map.md new file mode 100644 index 0000000..0646324 --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/07-version-pinning-map.md @@ -0,0 +1,92 @@ +# 07 · Version-pinning map + +**[This Project]** — *This is the most-consulted page in the wiki. Bookmark it.* + +Versions in this build live in **four files**, in **four different formats**, and several of them are duplicated. Bumping a single thing (e.g. Rider SDK 2026.1 → 2026.2) requires editing multiple places. This page is the canonical map. + +## The full table + +| Property | File | Line | Current | Drives | +|---|---|---|---|---| +| **Gradle wrapper** | `gradle/wrapper/gradle-wrapper.properties` | 3 | 9.4.1 | the Gradle build itself | +| **Gradle wrapper** *(duplicate)* | `build.gradle.kts` | 58 | 9.4.1 | regenerated on `./gradlew wrapper` | +| **JDK** | `build.gradle.kts` | 14-20 | 21 | Java toolchain (auto-provisioned) | +| **JVM target** *(redundant)* | `build.gradle.kts` | 82-84 | 21 | Kotlin compiler output | +| **JDK** *(stale)* | `.run/Build Plugin.run.xml` | 8 | corretto-17.0.7 | should be 21 — drift | +| **Kotlin** | `gradle/libs.versions.toml` | 2 | 2.3.20 | frontend Kotlin compiler | +| **`rdKotlinVersion`** | `gradle.properties` | 25 | 2.3.0 | Kotlin used by `pluginManagement` for rdgen plugin resolution | +| **`rdGen`** (library) | `gradle/libs.versions.toml` | 3 | 2026.1.3 | `com.jetbrains.rd:rd-gen` library inside `:protocol` | +| **`rdVersion`** (plugin) | `gradle.properties` | 24 | 2026.1 | the `com.jetbrains.rdgen` Gradle *plugin* | +| **IPGP** | `gradle.properties` | 26 | 2.14.0 | `intellijPlatformGradlePluginVersion` consumed by `pluginManagement` | +| **IPGP** *(duplicate, drifted)* | `build.gradle.kts` | 9 | 2.14.0 | the actually-applied plugin version | +| **`gradleJvmWrapperVersion`** | `gradle.properties` | 27 | 0.15.0 | `me.filippov.gradle.jvm.wrapper` plugin via `pluginManagement` | +| **`gradleJvmWrapperVersion`** *(duplicate, DRIFTED)* | `build.gradle.kts` | 10 | 0.16.0 | the actually-applied plugin version | +| **`ProductVersion`** | `gradle.properties` | 17 | 2026.1 | which Rider IDE artifact IPGP downloads | +| **`SdkVersion`** | `Directory.Build.props` | 4 | 2026.1.* | `JetBrains.Rider.SDK` / `Lifetimes` / `RdFramework` NuGet versions | +| **`WaveVersion`** *(computed)* | `Directory.Build.props` | 33-35 | 261.0.0* | `Wave` NuGet package (ReSharper compat) | +| **`PluginVersion`** | `gradle.properties` | 7 | 2025.1.10 | plugin marketplace release version | +| **.NET SDK** | `global.json` | — | 7.0.202 (rollForward latestMajor) | `dotnet build` | +| **`riderBaseVersion`** | `gradle.properties` | 28 | 2025.1 | **DEAD** — zero references; delete | + +## Visual map: which file controls which artifact + +```mermaid +graph LR + gp[gradle.properties] + tom[gradle/libs.versions.toml] + dbp[Directory.Build.props] + bgk[build.gradle.kts] + gjs[global.json] + + gp -->|PluginVersion| pluginzip[Plugin zip filename + plugin.xml version] + gp -->|ProductVersion| ridersdk[Rider SDK download via IPGP] + gp -->|rdVersion| rdgenplugin[com.jetbrains.rdgen Gradle plugin] + gp -->|rdKotlinVersion| pmkotlin[Kotlin used in pluginManagement] + gp -->|intellijPlatformGradlePluginVersion DUP| ipgp[IntelliJ Platform Gradle Plugin] + gp -->|gradleJvmWrapperVersion DUP DRIFTED| wrap[gradle-jvm-wrapper plugin] + bgk -->|hardcoded ID version| ipgp + bgk -->|hardcoded ID version| wrap + tom -->|kotlin| kotlinplugin[Kotlin Gradle plugin and stdlib] + tom -->|rdGen| rdgenlib[com.jetbrains.rd:rd-gen library in :protocol] + dbp -->|SdkVersion| nugetpkgs[JetBrains.Lifetimes / RdFramework / Rider.SDK NuGets] + dbp -->|WaveVersion computed| wavepkg[Wave NuGet for ReSharper-for-VS] + gjs -->|sdk.version| dotnet[.NET SDK that runs dotnet build] +``` + +## Why is it like this? + +A tour of the design constraints, since "just consolidate it" is harder than it sounds: + +1. **`pluginManagement` runs first.** The `pluginManagement` block in `settings.gradle.kts:3-46` runs during Gradle's *initialization* phase, before the version catalog (`libs.versions.toml`) is fully materialized for build-script consumption. So the plugin versions used inside `pluginManagement` (rdgen, IPGP, jvm-wrapper) historically had to come from `gradle.properties` via `String by settings`. (Gradle 7.4+ permits some catalog access in settings, but the migration is awkward — flagged in §24.) + +2. **`build.gradle.kts:9-10` re-pins the same plugins inline.** This is real drift, not a constraint. The root build file declares `id("org.jetbrains.intellij.platform") version "2.14.0"` and `id("me.filippov.gradle.jvm.wrapper") version "0.16.0"`. These re-applies happen because the root build script is its own scope — it can't trivially read from `pluginManagement.plugins`. Unless `pluginManagement` and the root `plugins { }` block agree on the version, you've got drift. Today: the IPGP versions agree (both 2.14.0); the jvm-wrapper versions don't (0.15.0 vs 0.16.0). The `gradle.properties` version of jvm-wrapper is **dead** — the inline value wins. + +3. **The .NET side has its own world.** `Directory.Build.props` and `global.json` are MSBuild's standard centralization mechanisms. They can't be unified with Gradle's catalog, but they CAN be kept in lockstep with a release process that bumps both together (§19's runbooks). + +4. **WaveVersion is computed**, not pinned. `Directory.Build.props:33-35`: + ```xml + $(SdkVersion.Substring(2,2))$(SdkVersion.Substring(5,1)) + $(WaveVersionBase).0.0$(SdkVersion.Substring(8)) + ``` + For `SdkVersion=2026.1.*`: `Substring(2,2) = "26"`, `Substring(5,1) = "1"` → `"261"`; `Substring(8) = "*"` → `WaveVersion = "261.0.0*"`. Wave 261 = ReSharper 2026.1. + +## Compatibility-matrix anchor URLs + +Each version has an upstream compatibility matrix you should consult before bumping. Linked here so you don't have to hunt: + +- **Kotlin** ↔ IntelliJ Platform: (also linked in `gradle/libs.versions.toml:2`) +- **rd / rdgen**: (also linked in `gradle/libs.versions.toml:3`) +- **IntelliJ Platform Gradle Plugin** changelog: +- **Rider build numbers** ↔ release names: +- **Gradle / JDK / Kotlin Gradle plugin compatibility**: + +## Drift state today + +- `intellijPlatformGradlePluginVersion`: synced (both 2.14.0). Safe. +- `gradleJvmWrapperVersion`: **drifted** (0.15.0 vs 0.16.0). The `gradle.properties` value is effectively dead. +- `riderBaseVersion`: **dead**. Zero references. Delete (§24.1). +- `.run/Build Plugin.run.xml` JDK: **stale** (corretto-17.0.7 vs toolchain 21). + +§24 has the consolidation refactor as a captured backlog item. + +→ Next: [08 · settings.gradle.kts and pluginManagement](08-settings-and-pluginManagement.md) diff --git a/docs/wiki/build-system/part-2-this-project/08-settings-and-pluginManagement.md b/docs/wiki/build-system/part-2-this-project/08-settings-and-pluginManagement.md new file mode 100644 index 0000000..1b8d2da --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/08-settings-and-pluginManagement.md @@ -0,0 +1,135 @@ +# 08 · settings.gradle.kts and pluginManagement + +**[This Project]** — *Mostly standard Gradle, with one specific JetBrains-flavoured workaround.* + +`settings.gradle.kts` is the script Gradle runs at *initialization* — before any `build.gradle.kts` files. It does three things in this repo: + +1. Names the root project (`rootProject.name = "rimworlddev"`) +2. Configures `pluginManagement` (where Gradle finds plugins, and what versions) +3. Declares subprojects (`include(":protocol")`) + +## The whole file at a glance + +`settings.gradle.kts:1-59`. Reading it in five chunks: + +### Chunk 1: project name (`:1`) + +```kotlin +rootProject.name = "rimworlddev" +``` + +This is the name that ends up in `build/distributions/rimworlddev-.zip` and the sandbox path `/dotnet/...` (used at `build.gradle.kts:175, 178`). Don't rename casually. + +### Chunk 2: pluginManagement properties (`:3-14`) + +```kotlin +pluginManagement { + val rdVersion: String by settings + val rdKotlinVersion: String by settings + val intellijPlatformGradlePluginVersion: String by settings + val gradleJvmWrapperVersion: String by settings + // ... and several "echo" properties (DotnetPluginId, etc.) +``` + +`String by settings` is the same property delegate as `by project`, but reads from `gradle.properties` *during initialization*. The settings script doesn't have a `Project` object yet — only a `Settings` — so the delegate target is different. + +The "echo" properties (`DotnetPluginId`, `DotnetSolution`, etc.) appear to be defensive: declared here but not actually read inside `pluginManagement`. Harmless but noise. + +### Chunk 3: pluginManagement repositories (`:16-27`) + +```kotlin +repositories { + maven("https://cache-redirector.jetbrains.com/intellij-dependencies") + maven("https://cache-redirector.jetbrains.com/plugins.gradle.org") + maven("https://cache-redirector.jetbrains.com/maven-central") + maven("https://cache-redirector.jetbrains.com/dl.bintray.com/kotlin/kotlin-eap") + maven("https://cache-redirector.jetbrains.com/myget.org.rd-snapshots.maven") + maven("https://cache-redirector.jetbrains.com/intellij-dependencies") // duplicate + + if (rdVersion == "SNAPSHOT") { + mavenLocal() + } +} +``` + +These tell Gradle *where to download Gradle plugins from*. All routed through JetBrains' cache redirector — a transparent CDN cache for upstream Maven repos. Works around upstream availability blips and gives JetBrains telemetry on what's being fetched. + +The `if (rdVersion == "SNAPSHOT") mavenLocal()` line lets a JetBrains employee build against a locally-published rd snapshot. You will essentially never need this. + +(There's a duplicate `intellij-dependencies` line — harmless but cleanup-able.) + +### Chunk 4: pluginManagement plugin versions (`:29-34`) + +```kotlin +plugins { + id("com.jetbrains.rdgen") version rdVersion + id("org.jetbrains.kotlin.jvm") version rdKotlinVersion + id("org.jetbrains.intellij.platform") version intellijPlatformGradlePluginVersion + id("me.filippov.gradle.jvm.wrapper") version gradleJvmWrapperVersion +} +``` + +This block declares **default versions** for plugins that subprojects might apply *without specifying a version themselves*. Notice `:protocol/build.gradle.kts:4` does: + +```kotlin +plugins { + id("org.jetbrains.kotlin.jvm") // no version — picks up default from here +} +``` + +The root `build.gradle.kts:9-10` overrides with explicit `version "..."` for IPGP and jvm-wrapper. That's the duplication / drift surface (§07). + +### Chunk 5: the rdgen coordinate hack (`:36-45`) + +```kotlin +resolutionStrategy { + eachPlugin { + // Gradle has to map a plugin dependency to Maven coordinates - '{groupId}:{artifactId}:{version}'. + // It tries to do '{plugin.id}:{plugin.id}.gradle.plugin:version'. + // This doesn't work for rdgen, so we provide some help + if (requested.id.id == "com.jetbrains.rdgen") { + useModule("com.jetbrains.rd:rd-gen:${requested.version}") + } + } +} +``` + +Gradle's plugin marketplace convention says plugin id `foo` lives at Maven coordinates `foo:foo.gradle.plugin`. The rdgen plugin doesn't follow that convention — it's published at `com.jetbrains.rd:rd-gen`. This block tells Gradle: *"when somebody asks for plugin id `com.jetbrains.rdgen`, fetch from `com.jetbrains.rd:rd-gen` instead"*. + +This is **canonical and required**, not a smell. You'll see the same pattern in every JetBrains-template repo using rdgen. + +### Chunk 6: dependencyResolutionManagement (`:47-57`) + +```kotlin +dependencyResolutionManagement { + repositories { + // ... same JetBrains cache-redirector mirrors + } +} +``` + +Where Gradle finds **library dependencies** (as opposed to Gradle plugins). Centralizing it here keeps `build.gradle.kts` tidy. + +### Chunk 7: subprojects (`:59`) + +```kotlin +include(":protocol") +``` + +The single subproject. The root project is itself the JVM frontend (its sources are in `src/rider/main/`); `:protocol` is the rdgen runner. There is no `:rider-frontend` subproject — the root *is* it. + +## Mental model + +| pluginManagement block | dependencyResolutionManagement block | +|---|---| +| Where Gradle finds **plugins** | Where Gradle finds **libraries** | +| Plugin **default versions** | n/a | +| Plugin id → Maven coords mapping | n/a | +| Runs at **initialization** | Used during **configuration** | +| Reads `String by settings` from `gradle.properties` | n/a | + +## Key takeaway + +You'd think `pluginManagement` reads from the version catalog (`libs.versions.toml`). It can — Gradle 7.4+ permits some access — but historically didn't, which is why this build's IPGP and rdgen plugin versions live in `gradle.properties` and are read via `String by settings`. The constraint has loosened, but the migration to put them all in the version catalog is one of §24's refactor opportunities, not done yet. + +→ Next: [09 · Annotated build.gradle.kts](09-annotated-build-gradle-kts.md) diff --git a/docs/wiki/build-system/part-2-this-project/09-annotated-build-gradle-kts.md b/docs/wiki/build-system/part-2-this-project/09-annotated-build-gradle-kts.md new file mode 100644 index 0000000..f74db7b --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/09-annotated-build-gradle-kts.md @@ -0,0 +1,436 @@ +# 09 · Annotated build.gradle.kts + +**[This Project]** — *The centerpiece. Every block of `build.gradle.kts` walked through with explanation.* + +This page is meant to be read with `build.gradle.kts` open beside you. Each section quotes the relevant lines, explains what's happening, and flags any "watch out for" issues. Line numbers are correct as of the worktree this wiki was authored in. + +--- + +## Imports (`:1-4`) + +```kotlin +import com.jetbrains.plugin.structure.base.utils.isFile +import org.apache.tools.ant.taskdefs.condition.Os +import org.jetbrains.intellij.platform.gradle.Constants +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +``` + +- `isFile` — a Kotlin extension on `java.nio.file.Path` from `com.jetbrains.plugin.structure`, transitively pulled in by IPGP. Used at `:262` to assert `rider-model.jar` exists. +- `Os` — Ant. Yes, Apache Ant. Gradle ships Ant under the hood for legacy reasons; this is the canonical way to detect the OS family in a Gradle script. Used at `:22` for `isWindows`. +- `Constants` — IPGP's task-name constants object. Used at `:267` (`Constants.Tasks.INITIALIZE_INTELLIJ_PLATFORM_PLUGIN`). +- `JvmTarget` — used at `:83` to set Kotlin's bytecode target. + +--- + +## `plugins { }` block (`:6-11`) + +```kotlin +plugins { + id("java") + alias(libs.plugins.kotlinJvm) + id("org.jetbrains.intellij.platform") version "2.14.0" + id("me.filippov.gradle.jvm.wrapper") version "0.16.0" +} +``` + +Three plugin-declaration shapes in one block: +- `id("java")` — built-in Gradle plugin, no version needed +- `alias(libs.plugins.kotlinJvm)` — version-catalog reference (resolves through `gradle/libs.versions.toml`'s `[plugins]` table) +- `id("...") version "..."` — explicit version declaration + +**Watch out:** the IPGP and jvm-wrapper plugin versions are *also* declared in `settings.gradle.kts` `pluginManagement.plugins` block, sourced from `gradle.properties`. Today the IPGP versions match (both 2.14.0); the jvm-wrapper ones don't (0.16.0 here vs 0.15.0 in `gradle.properties`). Real drift. The inline value (here) wins. See §07. + +--- + +## `java { }` block (`:14-20`) + +```kotlin +java { + sourceCompatibility = JavaVersion.VERSION_21 + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} +``` + +Two things: +- `sourceCompatibility` says "the .java sources in this project are written for Java 21" +- `toolchain` says "Gradle, find me a JDK 21 to use" (auto-provisions one if not present) + +The toolchain is what actually picks the JDK. `sourceCompatibility` mostly affects javac — and there are no `.java` sources here. Effectively redundant but harmless. + +`tasks.compileKotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }` at `:82-84` is a *third* JVM-target declaration, this time for Kotlin → bytecode. All three (toolchain, sourceCompatibility, JvmTarget) need to align. They do. + +--- + +## `extra["isWindows"]` (`:22-23`) + +```kotlin +val isWindows = Os.isFamily(Os.FAMILY_WINDOWS) +extra["isWindows"] = isWindows +``` + +`extra` is Gradle's loose property bag on the `Project` — a `MutableMap`. Storing `isWindows` here lets later blocks read it without recomputing. Used at `:188` in the DotFiles patcher. + +--- + +## `String by project` properties (`:25-31`) + +```kotlin +val DotnetSolution: String by project +val BuildConfiguration: String by project +val ProductVersion: String by project +val DotnetPluginId: String by project +val RiderPluginId: String by project +val PublishToken: String by project +val PluginVersion: String by project +``` + +Property delegates that read from `gradle.properties` (or `-P` CLI overrides). Eager read at configuration time — if any property is missing, the build fails the moment that line executes. + +`PublishToken` defaults to `"_PLACEHOLDER_"` in `gradle.properties:11`; CI overrides it via `-PPublishToken=$secret` (Deploy.yml). + +`RiderPluginId` is declared but not actually read in this file — only used to inject as a property elsewhere. Harmless echo. + +--- + +## Repositories (`:33-51`) + +Two blocks: + +**`allprojects.repositories`** (`:33-40`) — shared with `:protocol`: + +```kotlin +allprojects { + repositories { + maven("https://cache-redirector.jetbrains.com/intellij-dependencies") + maven("https://cache-redirector.jetbrains.com/intellij-repository/releases") + maven("https://cache-redirector.jetbrains.com/intellij-repository/snapshots") + maven("https://cache-redirector.jetbrains.com/maven-central") + } +} +``` + +**Top-level `repositories`** (`:42-51`) — for the root project, with IPGP-specific extras: + +```kotlin +repositories { + // (same four mavens) + intellijPlatform { + defaultRepositories() + jetbrainsRuntime() + } +} +``` + +`intellijPlatform { defaultRepositories() }` is IPGP's DSL adding the JetBrains-specific repos needed to resolve a Rider SDK artifact. `jetbrainsRuntime()` adds the JBR repo. Both are required for the `dependencies { intellijPlatform { rider(...); jetbrainsRuntime() } }` block at `:117-127` to resolve. + +The duplication (mavens listed in both `allprojects` and root) is harmless. The version-catalog-style `dependencyResolutionManagement` in `settings.gradle.kts:47-57` is a more modern alternative; the current shape works. + +--- + +## `apply { plugin("kotlin") }` (`:53-55`) + +```kotlin +apply { + plugin("kotlin") +} +``` + +**Redundant.** The Kotlin JVM plugin was already applied via `alias(libs.plugins.kotlinJvm)` at `:8`. This is dead cargo-cult code from a template ancestor. Flagged for cleanup in §17. + +--- + +## `tasks.wrapper { }` (`:57-62`) + +```kotlin +tasks.wrapper { + gradleVersion = "9.4.1" + distributionType = Wrapper.DistributionType.ALL + distributionUrl = + "https://cache-redirector.jetbrains.com/services.gradle.org/distributions/gradle-${gradleVersion}-all.zip" +} +``` + +Configures the built-in `wrapper` task. Running `./gradlew wrapper` regenerates `gradle/wrapper/gradle-wrapper.properties` to point at this version. The `cache-redirector` URL is JetBrains' mirror. + +**Watch out:** the Gradle version is also pinned in `gradle/wrapper/gradle-wrapper.properties:3`. They must agree. After bumping, run `./gradlew wrapper` *twice* — that's the canonical Gradle dance to fully update the wrapper scripts. + +--- + +## `version = ...` (`:64`) + +```kotlin +version = extra["PluginVersion"] as String +``` + +Sets the project's version. Used by IPGP when constructing artifact filenames (e.g. `rimworlddev-2025.1.10.zip`). + +--- + +## `sourceSets { }` (`:66-72`) + +```kotlin +sourceSets { + main { + java.srcDir("src/rider/main/java") + kotlin.srcDir("src/rider/main/kotlin") + resources.srcDir("src/rider/main/resources") + } +} +``` + +Overrides Gradle's default `src/main/{java,kotlin,resources}` paths to put everything under `src/rider/main/...`. Reason: the repo also holds `.NET` source under `src/dotnet/`, and segregating both languages by tier makes the layout legible. + +`src/rider/main/java/` is empty in practice — there are no `.java` files. Could be deleted for tidiness; harmless if left. + +--- + +## `instrumentCode/instrumentTestCode disabled` (`:74-80`) + +```kotlin +tasks.instrumentCode { enabled = false } +tasks.instrumentTestCode { enabled = false } +``` + +IPGP runs IntelliJ's bytecode instrumentation on plugin classes (NotNull annotations, form `*.form` files). It's been a historical source of CI flakes for plugins that don't use `.form` files. Disabling is fine for this plugin. + +--- + +## `compileKotlin compilerOptions` (`:82-84`) + +```kotlin +tasks.compileKotlin { + compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } +} +``` + +Tells the Kotlin compiler to emit JVM 21 bytecode. Aligns with `java { toolchain { 21 } }` at `:14-20`. + +--- + +## `compileDotNet` task (`:86-90`) + +```kotlin +val compileDotNet by tasks.registering(Exec::class) { + executable("dotnet") + workingDir(rootDir) + args("build", "--consoleLoggerParameters:ErrorsOnly", "--configuration", "Release") +} +``` + +The simplest of the .NET escape-hatch tasks. Just runs `dotnet build` from the repo root with errors-only logging. + +**Watch out:** no `@InputDirectory` / `@OutputDirectory` declarations. Gradle has no idea what files go in or come out. Consequence: `compileDotNet` re-runs every build. `dependsOn(compileDotNet)` from other tasks orders correctly but doesn't make this skippable. Captured as refactor opportunity in §24. + +--- + +## `buildResharperPlugin` task (`:92-105`) + +```kotlin +val buildResharperPlugin by tasks.registering(Exec::class) { + val arguments = mutableListOf() + arguments.add("msbuild") + arguments.add(DotnetSolution) + arguments.add("/t:Restore;Rebuild;Pack") + arguments.add("/v:minimal") + arguments.add("/p:PackageOutputPath=\"$rootDir/output\"") + arguments.add("/p:PackageVersion=$PluginVersion") + executable("dotnet") + args(arguments) + workingDir(rootDir) +} +``` + +Calls `dotnet msbuild` with three MSBuild targets (`Restore`, `Rebuild`, `Pack`) and two property overrides. The `Pack` target produces the `.nupkg` for the **Wave/ReSharper** flavour of the plugin (the .Rider csproj has `false` and won't pack). The output goes to `output/ReSharperPlugin.RimworldDev..nupkg`. + +This task is invoked by `Deploy.yml` to produce the ReSharper-marketplace artifact, but it's *not* part of `:buildPlugin`'s dependency chain — it's only run when explicitly requested (or by `Deploy.yml`). + +Same incrementality caveat as `compileDotNet`. + +--- + +## `tasks.buildPlugin { doLast { copy(...) } }` (`:107-114`) + +```kotlin +tasks.buildPlugin { + doLast { + copy { + from("${buildDir}/distributions/${rootProject.name}-${version}.zip") + into("${rootDir}/output") + } + } +} +``` + +`buildPlugin` is contributed by IPGP — it produces the Rider plugin ZIP at `build/distributions/-.zip`. This `doLast` copies the ZIP into `output/` so `Deploy.yml`'s GitHub Release upload can find it. + +**Watch out:** +- `${buildDir}` is **deprecated in Gradle 9** and will be **removed in Gradle 10**. Replace with `layout.buildDirectory.dir(...)` (a `Provider`). Flagged in §17. +- The `copy { }` here is `Project.copy(Action)`, an eager Gradle API, not the `Copy` task type. Acceptable inside `doLast`. + +--- + +## `dependencies { intellijPlatform { ... } }` (`:116-128`) + +```kotlin +dependencies { + intellijPlatform { + rider(ProductVersion) { + useInstaller = false + } + jetbrainsRuntime() + bundledPlugin("com.intellij.resharper.unity") + bundledModule("intellij.spellchecker") + } +} +``` + +This is IPGP's DSL inside the `dependencies { }` block. What each line does: + +- `rider(ProductVersion)` — declare a dependency on the Rider IDE artifact at version `2026.1` (from `gradle.properties:17`). IPGP downloads, extracts, and wires it onto the compile classpath. +- `useInstaller = false` — fetch from the JetBrains Maven repo (where artifact JARs live) rather than the binary CDN (where Rider installers live). The Maven path works with `actions/setup-java` Gradle caching in CI. Coupled with the `useBinaryReleases=false` flag in `gradle.properties:31`. +- `jetbrainsRuntime()` — pull in JBR (the matching JetBrains Runtime). Required because `runIde` launches a sandbox Rider, which needs JBR. +- `bundledPlugin("com.intellij.resharper.unity")` — make the Unity plugin's API surface available at compile time. The plugin descriptor (`plugin.xml:8`) declares a `` on this. +- `bundledModule("intellij.spellchecker")` — same idea, but the spellchecker has been moved from a "bundled plugin" to a "bundled module" in recent Platform versions. Module vs. plugin: bundled plugins have their own `plugin.xml` and an id; bundled modules are JAR-level units in `lib/modules/`. + +**Watch out:** the bundled-plugin → bundled-module migration is the kind of thing that breaks silently on a Platform upgrade. If you upgrade to a Platform version where `intellij.spellchecker` becomes something else, this line fails. JetBrains documents these migrations in IPGP changelogs; consult before bumping. + +--- + +## `intellijPlatform { pluginVerification { ... } }` (`:130-139`) + +```kotlin +intellijPlatform { + pluginVerification { + freeArgs = listOf("-mute", "TemplateWordInPluginId") + ides { +// ide(IntelliJPlatformType.Rider, ProductVersion) + recommended() + } + } +} +``` + +This is the **third** `intellijPlatform { }` scope you'll meet in this file (the others are inside `repositories { }` and `dependencies { }`). The bare top-level form configures plugin-wide settings. + +`pluginVerification` controls IPGP's `verifyPlugin` task, which runs JetBrains' Plugin Verifier against your built artifact across multiple IDE versions to catch binary-incompatibility issues. + +- `freeArgs = listOf("-mute", "TemplateWordInPluginId")` — silence a known false-positive rule that flags plugin IDs starting with "RimworldDev" because they happen to contain words from the JetBrains template +- `ides { recommended() }` — verify against JetBrains' "recommended" set of IDE versions. The commented-out alternative would pin a specific IDE+version + +`verifyPlugin` is not run on every build. CI doesn't currently invoke it. You can run it locally with `./gradlew verifyPlugin`. + +--- + +## `tasks.runIde { }` (`:141-154`) + +```kotlin +tasks.runIde { + dependsOn(compileDotNet) + maxHeapSize = "1500m" + autoReload = false + argumentProviders += CommandLineArgumentProvider { + listOf("${rootDir}/example-mod/AshAndDust.sln") + } +} +``` + +Configures IPGP's `runIde` task — the one that boots a sandboxed Rider with this plugin loaded. + +- `dependsOn(compileDotNet)` — ensure the .NET half is built first +- `maxHeapSize = "1500m"` — match Rider's default; IPGP's default of 512m is too small for Rider +- `autoReload = false` — Rider's backend doesn't support dynamic plugin reload (the .NET process can't safely swap code mid-flight). Disabling avoids a misleading auto-reload story +- `argumentProviders += CommandLineArgumentProvider { ... }` — pass `${rootDir}/example-mod/AshAndDust.sln` as an arg to the launched Rider, so it auto-opens the example mod + +The `argumentProviders` shape is Gradle's lazy-args idiom (§04). The lambda runs at execution time, capturing `rootDir` from configuration time. + +--- + +## `tasks.prepareSandbox { }` (`:156-224`) + +The biggest, fragilest block. Has its own page: see §14. + +In short: this task copies the .NET DLLs (and ProjectTemplates) into the sandboxed plugin folder, asserts the DLLs exist (failing the build if `compileDotNet` produced nothing), and on Mac/Linux patches missing Unity DotFiles DLLs from a local Rider install. + +--- + +## `testDotNet` task (`:226-230`) + +```kotlin +val testDotNet by tasks.registering(Exec::class) { + executable("dotnet") + args("test", DotnetSolution, "--logger", "GitHubActions") + workingDir(rootDir) +} +``` + +`dotnet test` against the solution, with the GitHubActions logger format (which the CI workflow ingests for nice annotations). + +Currently a near-no-op because `ReSharperPlugin.RimworldDev.Tests` is a stub (no `.cs` test files). The runner exits successfully because there's nothing to fail. + +--- + +## `tasks.publishPlugin { }` (`:232-236`) + +```kotlin +tasks.publishPlugin { + dependsOn(testDotNet) + dependsOn(tasks.buildPlugin) + token.set(PublishToken) +} +``` + +Configures IPGP's `publishPlugin` task to upload the built ZIP to the JetBrains Marketplace. + +- `dependsOn(testDotNet)` — gate publish on .NET tests (currently a no-op gate; will become real when tests exist) +- `dependsOn(tasks.buildPlugin)` — ensure the ZIP exists +- `token.set(PublishToken)` — Marketplace auth token. The default is `"_PLACEHOLDER_"`; CI overrides via `-PPublishToken=$secret` + +`token.set(...)` is the `Property` write idiom (§04). Don't try `token = "..."`. + +--- + +## `tasks.patchPluginXml { }` (`:238-252`) + +```kotlin +tasks.patchPluginXml { + val changelogText = file("${rootDir}/CHANGELOG.md").readText() + .lines() + .dropWhile { !it.trim().startsWith("##") } + .drop(1) + .takeWhile { !it.trim().startsWith("##") } + .filter { it.trim().isNotEmpty() } + .joinToString("\r\n") { + "
  • ${it.trim().replace(Regex("^\\*\\s+?"), "")}
  • " + }.trim() + + pluginVersion.set(PluginVersion) + changeNotes.set("
      \r\n$changelogText\r\n
    ") + untilBuild.set(provider { null }) +} +``` + +`patchPluginXml` is contributed by IPGP — it rewrites the plugin's `META-INF/plugin.xml` at build time with values you set here. + +- The Kotlin chain parses `CHANGELOG.md`: skip everything before the first `##` heading, take the section between it and the next `##`, strip blank lines, convert each surviving line into an `
  • ` HTML item. The result becomes the plugin's "what's new" block on the Marketplace. +- `pluginVersion.set(PluginVersion)` — overrides the `0.0.0` placeholder in `plugin.xml` +- `untilBuild.set(provider { null })` — clears the auto-computed upper-build-number bound. By default IPGP pins `until-build` to roughly the same major.minor as the SDK; setting it to null means the plugin claims to be compatible with all future Rider versions. **Risky in theory but pragmatic.** Trade-off is discussed in §17. + +**Watch out:** the `file(...).readText()` runs at *configuration time*. That means every Gradle invocation reads `CHANGELOG.md`, even no-op ones. Ideally wrap in `provider { }` for laziness and config-cache friendliness. Flagged in §17. + +--- + +## `riderModel` configuration + `artifacts { }` (`:254-269`) + +The deepest Gradle rabbit hole in the project. Has its own page: see §13. + +In short: this declares a custom Gradle `Configuration` named `riderModel` exposing `lib/rd/rider-model.jar` from the extracted Rider SDK to the `:protocol` subproject. The `builtBy(INITIALIZE_INTELLIJ_PLATFORM_PLUGIN)` line ensures the SDK is downloaded before consumers try to read the JAR. + +--- + +## End + +That's every block. The hardest concepts — `prepareSandbox` (§14), `riderModel` (§13), the dual-csproj pattern referenced by `compileDotNet`/`prepareSandbox` (§12), and rdgen (§10) — get their own pages. + +→ Next: [10 · The protocol subproject and rdgen](10-the-protocol-subproject-and-rdgen.md) diff --git a/docs/wiki/build-system/part-2-this-project/10-the-protocol-subproject-and-rdgen.md b/docs/wiki/build-system/part-2-this-project/10-the-protocol-subproject-and-rdgen.md new file mode 100644 index 0000000..1444ea7 --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/10-the-protocol-subproject-and-rdgen.md @@ -0,0 +1,178 @@ +# 10 · The protocol subproject and rdgen + +**[This Project]** — *This is unique to Rider plugins. rdgen / RD doesn't exist in standard IntelliJ plugins.* + +The `:protocol` subproject is small but conceptually dense. Its only job is to run **rdgen**, JetBrains' protocol code generator, which turns a single Kotlin DSL file into matched Kotlin and C# source files. Those generated files are how the JVM frontend and .NET backend speak to each other. + +## What rdgen does + +You write one Kotlin file describing the protocol — calls, signals, properties, structs. rdgen reads it and produces: + +- A **Kotlin** file the frontend imports +- A **C#** file the backend imports + +Both files describe the same wire format in their respective languages. At runtime, the RD framework on each side binds to a shared pipe and marshals method invocations across. + +``` + protocol/src/main/kotlin/model/rider/Model.kt + │ + :protocol:rdgen + │ + ┌────────────┴────────────┐ + ▼ ▼ + RemodderProtocolModel.Generated.kt RemodderProtocolModel.Generated.cs + (JVM frontend uses this) (.NET backend uses this) +``` + +## The model file + +`protocol/src/main/kotlin/model/rider/Model.kt`: + +```kotlin +package model.rider + +import com.jetbrains.rd.generator.nova.Ext +import com.jetbrains.rd.generator.nova.csharp.CSharp50Generator +import com.jetbrains.rd.generator.nova.kotlin.Kotlin11Generator +import com.jetbrains.rd.generator.nova.* +import com.jetbrains.rd.generator.nova.PredefinedType.* +import com.jetbrains.rider.model.nova.ide.SolutionModel + +object RemodderProtocolModel : Ext(SolutionModel.Solution) { + init { + setting(CSharp50Generator.Namespace, "ReSharperPlugin.RdProtocol") + setting(Kotlin11Generator.Namespace, "com.jetbrains.rider.plugins.rdprotocol") + + // Remote procedure on backend + call("decompile", array(string), array(string)).async + } +} +``` + +What's happening: + +- `object RemodderProtocolModel : Ext(SolutionModel.Solution)` — declares an *extension* of the Rider **Solution-scoped** protocol. "Ext" means "this protocol attaches to a host scope"; the host scope here is `SolutionModel.Solution`, so the protocol exists per open solution. Other scopes you'd extend in different plugins: `IdeRoot` (application-wide), `RdSolution` (alternative solution scope), etc. +- `setting(...Namespace, "...")` — output namespace for each language +- `call("decompile", array(string), array(string)).async` — declares an asynchronous RPC named `decompile` taking `string[]` and returning `string[]` + +That's literally all this plugin's protocol does today: one async call. + +## The :protocol build script + +`protocol/build.gradle.kts`: + +```kotlin +plugins { + id("org.jetbrains.kotlin.jvm") + id("com.jetbrains.rdgen") version libs.versions.rdGen +} + +dependencies { + implementation(libs.kotlinStdLib) + implementation(libs.rdGen) + implementation( + project( + mapOf( + "path" to ":", + "configuration" to "riderModel" + ) + ) + ) +} + +val DotnetPluginId: String by rootProject +val RiderPluginId: String by rootProject + +rdgen { + val csOutput = File(rootDir, "src/dotnet/${DotnetPluginId}") + val ktOutput = File(rootDir, "src/rider/main/kotlin/remodder") + verbose = true + packages = "model.rider" + + generator { + language = "kotlin" + transform = "asis" + root = "com.jetbrains.rider.model.nova.ide.IdeRoot" + namespace = "com.jetbrains.rider.model" + directory = "$ktOutput" + } + + generator { + language = "csharp" + transform = "reversed" + root = "com.jetbrains.rider.model.nova.ide.IdeRoot" + namespace = "JetBrains.Rider.Model" + directory = "$csOutput" + } +} + +tasks.withType { + val classPath = sourceSets["main"].runtimeClasspath + dependsOn(classPath) + classpath(classPath) +} +``` + +Walk-through of the non-obvious parts: + +### `version libs.versions.rdGen` (`:5`) + +`libs.versions.rdGen` is a `Provider`. The `version` keyword in the plugin DSL accepts both `String` and `Provider`, so this works. Resolves to `2026.1.3` from `gradle/libs.versions.toml:3`. + +### `implementation(project(mapOf("path" to ":", "configuration" to "riderModel")))` (`:11-18`) + +Most subprojects depend on each other with `implementation(project(":root"))`. Here we're saying: depend on the root project, but specifically on its `riderModel` configuration — not the default `runtimeElements` or `apiElements`. + +That `riderModel` configuration is declared in the root `build.gradle.kts:254-269` and exposes `lib/rd/rider-model.jar` from inside the extracted Rider SDK. The `:protocol` module needs that JAR on its classpath because `Model.kt:8` extends `SolutionModel.Solution` defined inside it. See §13 for the full bridge mechanics. + +### Two `generator { }` blocks (`:31-45`) + +One per output language. Notable settings: + +- `language = "kotlin" | "csharp"` — picks the codegen backend +- `transform = "asis" | "reversed"` — the **call direction**. `"asis"` keeps the original direction of declared calls; `"reversed"` flips it. Why: a `call("decompile", ...)` in the model is, semantically, "the frontend asks the backend to decompile". The Kotlin output (consumed by frontend) wants `decompile.start(...)` for *initiating* the call (asis); the C# output (consumed by backend) wants `decompile.SetAsync(...)` for *handling* it (reversed). The two transforms produce mirror-image bindings. +- `root = "com.jetbrains.rider.model.nova.ide.IdeRoot"` — anchor type for code generation, defined in `rider-model.jar` +- `namespace` — package/namespace of the generated *root model*. The protocol-specific namespace was set inside `Model.kt:12-13` +- `directory` — where to drop the `.kt` / `.cs` files + +### `tasks.withType { dependsOn(classPath); classpath(classPath) }` (`:48-52`) + +`RdGenTask` is a Gradle task type provided by the rdgen plugin. We need to wire **the `:protocol` module's own compiled Kotlin classes** onto the rdgen task's classpath, because rdgen reflects on those classes (the `RemodderProtocolModel` object) at run time to discover the model. + +`tasks.withType { ... }` configures all tasks of that type lazily — including any added later. Gradle pattern: when a plugin adds tasks of a type, configure them this way to be future-proof. + +## Why the generated files are committed + +Both `RemodderProtocolModel.Generated.kt` (`src/rider/main/kotlin/remodder/`) and `RemodderProtocolModel.Generated.cs` (`src/dotnet/ReSharperPlugin.RimworldDev/`) live in git. + +**This is intentional.** Reasons: + +1. **rdgen is not on the build path of `:buildPlugin` / `:compileKotlin`.** It only runs when you explicitly invoke `./gradlew :protocol:rdgen`. So a contributor who hasn't run it can still build the plugin. +2. **Auditable PR diffs.** When the protocol changes, the diff is visible in code review — both halves' bindings are part of the patch. +3. **No hard dependency on the rdgen toolchain at build time.** A contributor doesn't need a working rdgen environment just to compile the plugin. + +The trade-off: you must remember to re-run `./gradlew :protocol:rdgen` after editing `Model.kt` and commit the regenerated files. CI does not regenerate; it consumes what's checked in. + +## How to add a new RPC + +1. Edit `protocol/src/main/kotlin/model/rider/Model.kt`. Add a `call(...)`, `signal(...)`, or `property(...)` inside `init { }`. +2. Run `./gradlew :protocol:rdgen`. +3. The regenerated `*.Generated.kt` and `*.Generated.cs` will appear with new symbols. +4. Implement the call site (frontend) and handler (backend). +5. Commit the regenerated files alongside your protocol edit. + +## Threading and the `.async` modifier + +The current `decompile` call has `.async` appended: + +```kotlin +call("decompile", array(string), array(string)).async +``` + +`.async` means: the handler can complete the call later (return a `Task` / `Promise`); the framework won't block waiting. Without `.async`, the call is synchronous and the handler must produce the result on the same thread it was invoked on. + +On the Kotlin side, the generated `RemodderProtocolModel.Generated.kt` registers `extThreading` for the call. On the C# side, the handler is registered via `model.Decompile.SetAsync(...)` and runs under a `ReadLockCookie.Create()`-scoped operation in `RemodderComponent.cs`. + +You don't need to remember the details. The pattern is: **declarative threading in the model, mechanical bindings on both sides, `.async` flag for non-blocking calls.** + +→ Next: [11 · The .NET side](11-the-dotnet-side.md) diff --git a/docs/wiki/build-system/part-2-this-project/11-the-dotnet-side.md b/docs/wiki/build-system/part-2-this-project/11-the-dotnet-side.md new file mode 100644 index 0000000..a477acf --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/11-the-dotnet-side.md @@ -0,0 +1,150 @@ +# 11 · The .NET side + +**[This Project]** — *This is unique to Rider plugins. A standard IntelliJ plugin has no `.csproj` in its build at all.* + +This page covers the .NET / C# half of the build: `Directory.Build.props`, the `.csproj` files in general, `global.json`, and how `dotnet build` is wired into the Gradle world. + +(The dual-csproj pattern — *why* there are two `.csproj` files for one source tree — gets its own page in §12.) + +## `Directory.Build.props` — MSBuild's central config + +MSBuild auto-imports a file called `Directory.Build.props` into every `.csproj` in the tree (walking up the directory hierarchy). It's the .NET equivalent of `gradle.properties` for centralized configuration. + +This repo's `Directory.Build.props` (54 lines) does five things: + +### 1. Pin the SDK version (`:3-4`) + +```xml + + 2026.1.* + + +``` + +`SdkVersion=2026.1.*` is a **wildcard NuGet version**. `2026.1.*` resolves to the latest 2026.1.x patch on the configured NuGet feed. Used downstream in PackageReferences (`:39-46`). + +### 2. Customize MSBuild output paths (`:18-26`) + +```xml +true +false +None + +obj\$(MSBuildProjectName)\ +$(DefaultItemExcludes);obj\** +bin\$(MSBuildProjectName)\$(Configuration)\ +``` + +Notable: +- `bin\$(MSBuildProjectName)\$(Configuration)\` — output goes to `bin///`. So building `ReSharperPlugin.RimworldDev.Rider.csproj` with Release config produces `bin/ReSharperPlugin.RimworldDev.Rider/Release/`. This is what Gradle's `prepareSandbox` reads from (`build.gradle.kts:159`). +- `false` — by default MSBuild adds `/net6.0/` to the output path; this turns it off, keeping paths short. +- The "WarnOrError on architecture mismatch" suppression silences false positives common in mixed-architecture NuGet packages. + +### 3. Configuration-conditional defines (`:28-30`) + +```xml + + TRACE;DEBUG;JET_MODE_ASSERT + +``` + +In Debug, additional `#define`s are set. `JET_MODE_ASSERT` enables JetBrains assertion macros. Release builds don't get these. + +### 4. Compute `WaveVersion` (`:32-36`) + +```xml + + $(SdkVersion.Substring(2,2))$(SdkVersion.Substring(5,1)) + $(WaveVersionBase).0.0$(SdkVersion.Substring(8)) + $(WaveVersionBase).9999.0 + +``` + +For `SdkVersion=2026.1.*`: +- `Substring(2,2)` = `"26"`, `Substring(5,1)` = `"1"` → `WaveVersionBase = "261"` +- `Substring(8)` of `"2026.1.*"` = `"*"` → `WaveVersion = "261.0.0*"` + +Wave is ReSharper's internal version stream. Wave 261 = ReSharper 2026.1, Wave 252 = 2025.2. The `Wave` NuGet package referenced by the Wave/ReSharper csproj uses this. JetBrains' Marketplace enforces Wave-compatible NuGet manifests at upload. + +### 5. Pin the JetBrains NuGet packages (`:38-47`) + +```xml + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + +``` + +Every `.csproj` inheriting these props gets: +- `JetBrains.Annotations` — `[NotNull]`, `[CanBeNull]`, etc. attributes +- `JetBrains.Lifetimes` — RD lifetime management +- `JetBrains.RdFramework` — RD runtime +- `JetBrains.Rider.SDK` — Rider extension APIs + +`Lifetimes` and `RdFramework` are **explicitly pinned to `$(SdkVersion)`** even though they'd come in transitively. Why: the RD `serializationHash` in the generated C# (RemodderProtocolModel.Generated.cs:73 in this codebase) is computed against a specific RdFramework version. If transitive resolution picks a different RdFramework patch, frontend and backend hashes mismatch and the protocol bind silently fails. Explicit pinning is a binary-compat backstop. + +`PrivateAssets="all"` on the Rider SDK means "consumers of this project don't transitively get the Rider SDK." `IncludeAssets="runtime; build; ..."` lists what *this* project pulls in. Together they say "I get everything; consumers get nothing" — appropriate for a leaf plugin assembly. + +## `global.json` — pin the .NET SDK + +```json +{ + "sdk": { + "version": "7.0.202", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} +``` + +`dotnet` CLI reads this and uses .NET SDK 7.0.202+ (with `rollForward: latestMajor` it'll roll up to whatever's installed in the 7.x or higher line). On a CI runner with .NET 8 or 9 installed, this is fine. + +## How Gradle and dotnet interact + +Three Gradle `Exec` tasks bridge the worlds: + +| Task | Command | What it produces | +|---|---|---| +| `compileDotNet` (`build.gradle.kts:86-90`) | `dotnet build --configuration Release` | Built DLLs in `bin//Release/` | +| `buildResharperPlugin` (`:92-105`) | `dotnet msbuild $sln /t:Restore;Rebuild;Pack /p:PackageOutputPath=output /p:PackageVersion=$ver` | `output/ReSharperPlugin.RimworldDev..nupkg` (Wave flavour only) | +| `testDotNet` (`:226-230`) | `dotnet test $sln --logger GitHubActions` | Test results in stdout | + +All three run from the repo root (`workingDir(rootDir)`). All three use `executable("dotnet")`, relying on `dotnet` being on PATH. + +## What ends up in the plugin ZIP + +After `compileDotNet`, the on-disk layout in `bin/` is roughly: + +``` +bin/ +├── ReSharperPlugin.RimworldDev/Release/ ← Wave/ReSharper flavour +│ └── ReSharperPlugin.RimworldDev.dll +└── ReSharperPlugin.RimworldDev.Rider/Release/ ← Rider flavour + ├── ReSharperPlugin.RimworldDev.dll (NOTE same name, different content) + ├── ReSharperPlugin.RimworldDev.pdb + ├── 0Harmony.dll + ├── AsmResolver.dll + ├── AsmResolver.DotNet.dll + ├── AsmResolver.PE.dll + ├── AsmResolver.PE.File.dll + ├── ICSharpCode.Decompiler.dll + └── (other transitive runtime DLLs) +``` + +`prepareSandbox` reads from the `.Rider/Release/` folder and copies a hand-picked subset into the plugin's `dotnet/` folder. See §14. + +## Why .NET 6 and not .NET 8 or 9? + +The `net6.0` in both `.csproj` files matches what the Rider 2026.1 backend host runs on. Plugins must target a TFM compatible with the host. JetBrains documents this in IPGP / Rider plugin docs. Bumping requires JetBrains to bump the host first. + +## .NET package: how it ships separately + +The `Pack` MSBuild target on the Wave csproj produces `output/ReSharperPlugin.RimworldDev..nupkg`. That `.nupkg` ships to the Marketplace under a separate plugin id (`ReSharperPlugin.RimworldDev`) for ReSharper-for-VS users. The Rider users get the `rimworlddev-.zip` instead. Two listings, two artifacts. Discussed in §16. + +→ Next: [12 · Dual-csproj pattern](12-dual-csproj-pattern.md) diff --git a/docs/wiki/build-system/part-2-this-project/12-dual-csproj-pattern.md b/docs/wiki/build-system/part-2-this-project/12-dual-csproj-pattern.md new file mode 100644 index 0000000..7e99e59 --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/12-dual-csproj-pattern.md @@ -0,0 +1,106 @@ +# 12 · Dual-csproj pattern + +**[This Project]** — *This is a JetBrains-plugin-specific convention. Standard .NET projects almost never do this.* + +Two `.csproj` files share the same source tree at `src/dotnet/ReSharperPlugin.RimworldDev/`. They produce two different DLLs from the same `.cs` files, gated by preprocessor symbols and `` rules. This page explains why, and what each csproj uses. + +## The two flavours + +| File | Flavour | Target consumer | Packs? | +|---|---|---|---| +| `ReSharperPlugin.RimworldDev.csproj` | "Wave" / ReSharper-for-VS | Visual Studio + ReSharper | Yes (produces a `.nupkg`) | +| `ReSharperPlugin.RimworldDev.Rider.csproj` | Rider | JetBrains Rider | No (consumed by Gradle's `prepareSandbox`) | + +Both produce a DLL named **`ReSharperPlugin.RimworldDev.dll`** (yes, the same name) but they go to different output folders thanks to the `bin\$(MSBuildProjectName)\$(Configuration)\` rule in `Directory.Build.props:25`. + +## What's actually different + +### Wave / ReSharper (`ReSharperPlugin.RimworldDev.csproj`) + +```xml +net6.0 +True +$(DefineConstants);RESHARPER +false + + + + + + + + + + + + + +``` + +Notable choices: +- **`DefineConstants RESHARPER`** — the source can do `#if RESHARPER ... #endif` for Wave-specific code paths +- **`IsPackable=True`** + custom `` items — produces a `.nupkg` with the DLL+PDB at `dotFiles/` (the path layout the Marketplace expects for ReSharper plugins) +- **``** — pulls in *only* the ReSharper SDK (no Rider-specific APIs). Note that this overrides the project-wide `JetBrains.Rider.SDK` from `Directory.Build.props:43` for this csproj. +- **``** — declares Wave-version compatibility. The Marketplace gates ReSharper plugin uploads on Wave compatibility metadata. +- **``** etc. — physically excludes those source folders from compilation. The Remodder, RimworldXmlProject, and ProjectTemplates features use Rider-specific APIs (RD protocol, project model) that aren't available in plain ReSharper. So they're excluded from the Wave build entirely. + +### Rider (`ReSharperPlugin.RimworldDev.Rider.csproj`) + +```xml +net6.0 +ReSharperPlugin.RimworldDev +$(AssemblyName) +false +$(DefineConstants);RIDER + + +true + + + + + + + + + +``` + +Notable choices: +- **`ReSharperPlugin.RimworldDev`** — forces the output DLL filename to match the Wave csproj's, so consumer code can reference `ReSharperPlugin.RimworldDev.dll` regardless of flavour. The csproj **filename** is `.Rider.csproj`; the **assembly name** is plain. +- **`DefineConstants RIDER`** — for code paths that should compile in Rider but not in plain ReSharper +- **`IsPackable=false`** — this DLL is not published to the Marketplace as a `.nupkg`. It's consumed by Gradle's `prepareSandbox` and packaged into the Rider plugin ZIP. +- **`true`** — without this, NuGet transitive DLLs (AsmResolver, ICSharpCode.Decompiler, etc.) wouldn't appear in `bin/`. The flag forces them into `bin/`, where Gradle's `prepareSandbox` (`build.gradle.kts:160-171`) finds and copies them. Comment in the csproj literally says: *"This is needed to force our dependant DLLs to be present in the build folder, which we then copy over in gradle."* +- **Remodder NuGets** — included only here. The Wave flavour excludes Remodder code via ``, so it doesn't need these. +- **``** — Krafs.Publicizer rewrites `0Harmony.dll`'s metadata to make private members public, so this code can reflect into Harmony internals. Harmless; affects the in-build copy only. + +## Decision matrix: where does new code go? + +| New feature uses... | Where to add it | Compile-Remove notes | +|---|---|---| +| ReSharper-only APIs (analyzer, completion item provider) | Both flavours, no exclusion | Code shared as-is | +| Rider-only APIs (project model, RD protocol, IRiderTooling) | Rider flavour only | Add file under e.g. `Remodder/` and ensure the Wave csproj excludes via `` | +| Decompiler / AsmResolver | Rider only | Same | +| Pure C# logic, no IDE APIs | Both | Code shared as-is | +| `#if RESHARPER` vs `#if RIDER` | Both, with conditional code | Use sparingly; prefer file-level exclusion | + +When you add a *new* folder of Rider-only code, remember to add the matching `` to the Wave csproj. Otherwise the Wave build will try to compile it and fail. + +## Why not just one csproj with `#if`? + +You could. JetBrains' template historically used both approaches. The dual-csproj pattern wins when: + +- Most of the divergent code is large (whole folders), making `#if` blocks unwieldy +- The package metadata differs significantly (different `PackageReference`s, different `IsPackable` story) +- You want clean separation between "what ships to ReSharper" and "what ships to Rider" + +This plugin makes that trade-off the right way: the Remodder feature uses heavy Rider-specific APIs and pulls in 5 NuGets that the Wave build doesn't need. Excluding it as a folder is cleaner than `#if`-fencing every method. + +## How the build picks the right one + +- `dotnet build` (the `compileDotNet` task) builds **everything** in the solution — both csprojs. Both DLLs end up in `bin/`. +- `dotnet msbuild .../$sln /t:Pack` (the `buildResharperPlugin` task) packs only csprojs with `IsPackable=true` — i.e., only the Wave one. Result: one `.nupkg`. +- `prepareSandbox` reads from `bin/ReSharperPlugin.RimworldDev.Rider/Release/` (`build.gradle.kts:159`) — so the Rider flavour is what gets bundled into the Rider plugin ZIP. +- `Deploy.yml` ships the ZIP (Rider) and the `.nupkg` (ReSharper) to the same Marketplace under different listings. + +→ Next: [13 · The riderModel bridge](13-the-riderModel-bridge.md) diff --git a/docs/wiki/build-system/part-2-this-project/13-the-riderModel-bridge.md b/docs/wiki/build-system/part-2-this-project/13-the-riderModel-bridge.md new file mode 100644 index 0000000..e835949 --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/13-the-riderModel-bridge.md @@ -0,0 +1,103 @@ +# 13 · The `riderModel` bridge + +**[This Project]** — *Idiomatic JetBrains pattern, but obscure if you've never seen it. Visible in `gradle-template`, `resharper-unity`, `azure-toolkit`. Worth understanding once.* + +The deepest Gradle rabbit hole in the project. Fifteen lines of DSL implementing what's conceptually a one-line idea: *"the `:protocol` subproject needs `lib/rd/rider-model.jar` from inside the Rider SDK on its compile classpath."* + +## The problem + +`protocol/src/main/kotlin/model/rider/Model.kt:8` declares: + +```kotlin +import com.jetbrains.rider.model.nova.ide.SolutionModel + +object RemodderProtocolModel : Ext(SolutionModel.Solution) { ... } +``` + +`SolutionModel.Solution` is defined inside a JAR called `rider-model.jar`, which lives inside the Rider SDK that IPGP downloads. To compile `Model.kt`, that JAR must be on the `:protocol` compile classpath. + +There's no published Maven artifact for `rider-model.jar` you could just `implementation(...)`. It only exists *inside* the extracted Rider SDK directory. So the build has to do something custom. + +## The solution + +The root project publishes a custom Gradle `Configuration` named `riderModel` that exposes the JAR. The `:protocol` module consumes from that configuration. + +### Producer side: `build.gradle.kts:254-269` + +```kotlin +val riderModel: Configuration by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false +} + +artifacts { + add(riderModel.name, provider { + intellijPlatform.platformPath.resolve("lib/rd/rider-model.jar").also { + check(it.isFile) { + "rider-model.jar is not found at $riderModel" + } + } + }) { + builtBy(Constants.Tasks.INITIALIZE_INTELLIJ_PLATFORM_PLUGIN) + } +} +``` + +What each line does: + +- `val riderModel: Configuration by configurations.creating { ... }` — creates a new Configuration on the root project named `riderModel`. A *Configuration* in Gradle is a typed bucket of dependencies and artifacts — used for compile classpaths, runtime classpaths, custom artifact channels. +- `isCanBeConsumed = true` — other projects can depend on this configuration and pull artifacts from it +- `isCanBeResolved = false` — this project itself can NOT use this configuration to resolve dependencies into a classpath. We're publishing a one-way channel. +- `artifacts { add(riderModel.name, provider { ... }) { ... } }` — register an artifact in the configuration. The artifact is a single file: `intellijPlatform.platformPath.resolve("lib/rd/rider-model.jar")`. +- The `provider { }` wrapper makes the file lookup lazy. Necessary because `intellijPlatform.platformPath` doesn't exist until IPGP's initialize task has extracted the SDK. +- `check(it.isFile) { ... }` is a fail-fast assertion. If the JAR isn't where we expect, you get a clear error instead of a baffling NoClassDefFoundError later. +- `builtBy(Constants.Tasks.INITIALIZE_INTELLIJ_PLATFORM_PLUGIN)` — tells Gradle this artifact is "produced by" the IPGP initialize task. Without this, Gradle would try to resolve the artifact before the SDK is downloaded — `rider-model.jar` doesn't exist yet, the assertion fires, build crashes. + +### Consumer side: `protocol/build.gradle.kts:11-18` + +```kotlin +implementation( + project( + mapOf( + "path" to ":", + "configuration" to "riderModel" + ) + ) +) +``` + +The `project(mapOf("path" to ":", "configuration" to "riderModel"))` syntax means: depend on the **root project**, but pull from its `riderModel` configuration specifically — not the default `runtimeElements` or `apiElements`. The map form is Gradle's typed-projection mechanism. + +`implementation(...)` then puts that artifact (the `rider-model.jar` file) on the `:protocol` compile classpath. `Model.kt` can now `import com.jetbrains.rider.model.nova.ide.SolutionModel`. + +## Why `builtBy` and not `dependsOn`? + +`dependsOn` is task-to-task ordering. `builtBy` is artifact-level dependency ordering — "this artifact's existence depends on that task running first." When `:protocol`'s compile-classpath resolver asks "where's the JAR?", Gradle checks the `builtBy` declaration, sees IPGP's initialize task hasn't run, schedules it, and waits. + +If you used `dependsOn` instead, you'd have to wire it onto every consumer task that resolves the configuration — much more fragile. The artifact-level pattern lets the data dependency drive the ordering automatically. + +## Why this looks weird + +A reader from a regular Gradle multi-project comes here knowing `implementation(project(":sub"))` and wonders what the configuration map is for. The answer: regular Gradle projects expose their `runtimeElements`/`apiElements` configurations by default, which contain the project's compiled JAR and the dependencies it needs at runtime. This use case isn't that — we're exposing a single file extracted from someone else's archive. So we declare a custom configuration and tell consumers to opt into it explicitly. + +## Why this is canonical + +This pattern shows up in many JetBrains-template-derived plugins: +- [`gradle-template`](https://github.com/JetBrains/rider-plugin-template) +- [`resharper-unity`](https://github.com/JetBrains/resharper-unity) +- Various JetBrains internal Rider plugins + +If you've seen it once you'll recognize it. The "consumer-only configuration → publish a single SDK JAR → builtBy initialize" trio is standard for any Rider plugin that uses rdgen. + +## What goes wrong if you break it + +- Drop the `builtBy(...)`: random "rider-model.jar is not found" failures during clean builds. The `check { }` assertion catches it, but only with a vague error. +- Drop `isCanBeConsumed = true` (set both to `false`): nothing can pull from the configuration; `:protocol` fails to resolve. +- Drop `isCanBeResolved = false` and try to use the configuration locally: Gradle warns about a configuration that's both consumed-and-resolved (an anti-pattern called "legacy configurations"). +- Try to do this without a configuration (e.g., directly add `files(...)` to `:protocol`'s classpath): you'd lose the artifact-level dependency ordering and have to add a `dependsOn` chain to every consumer. + +## TL;DR mental model + +> The root project owns the Rider SDK extraction. It exposes a typed pipe to the `:protocol` subproject saying "here's `rider-model.jar`, and don't try to read it before I've extracted the SDK." The `:protocol` subproject consumes from the typed pipe. That's all the `riderModel` configuration is. + +→ Next: [14 · prepareSandbox — the glue](14-prepareSandbox-the-glue.md) diff --git a/docs/wiki/build-system/part-2-this-project/14-prepareSandbox-the-glue.md b/docs/wiki/build-system/part-2-this-project/14-prepareSandbox-the-glue.md new file mode 100644 index 0000000..9c3e12b --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/14-prepareSandbox-the-glue.md @@ -0,0 +1,198 @@ +# 14 · prepareSandbox — the glue + +**[This Project]** — *The most fragile and most load-bearing block in `build.gradle.kts`. Read this BEFORE you change anything in the file.* + +`prepareSandbox` is the task that physically lays out the plugin's files into a fake plugins directory the way Rider expects them. It's contributed by IPGP, configured in this repo at `build.gradle.kts:156-224`. The block does **two distinct things in one place**: declarative file copying (Gradle's Copy machinery) and imperative post-actions (existence checks + DotFiles patching). + +## What "the sandbox" is + +Run `./gradlew runIde` and IPGP creates `build/idea-sandbox/` containing: + +``` +build/idea-sandbox/ +├── config/ ← IDE settings (config dir) +├── system/ ← caches, indexes +└── plugins/ + └── rimworlddev/ ← THIS IS WHAT prepareSandbox FILLS IN + ├── lib/ + │ └── rimworlddev.jar ← compiled Kotlin + ├── dotnet/ ← THE LOAD-BEARING CONVENTION + │ ├── ReSharperPlugin.RimworldDev.dll + │ ├── ReSharperPlugin.RimworldDev.pdb + │ ├── 0Harmony.dll + │ ├── AsmResolver.dll + │ └── (more) + └── ProjectTemplates/ + └── RimworldProjectTemplate/ + └── ... +``` + +When the launched Rider boots, it discovers `build/idea-sandbox/plugins/rimworlddev/` and treats it as an installed plugin. The frontend JARs go into the JVM. The DLLs in `dotnet/` get pushed into the backend's plugin scope. + +The `dotnet/` folder name is **a load-bearing convention** — Rider's plugin loader sweeps `/dotnet/*.dll` automatically. That's why `prepareSandbox` copies into `${rootProject.name}/dotnet` (`build.gradle.kts:175`). + +## The block, walked + +`build.gradle.kts:156-224`. Three parts. + +### Part 1: ordering and inputs (`:157-178`) + +```kotlin +tasks.prepareSandbox { + dependsOn(compileDotNet) + + val outputFolder = "${rootDir}/src/dotnet/${DotnetPluginId}/bin/${DotnetPluginId}.Rider/${BuildConfiguration}" + val dllFiles = listOf( + "$outputFolder/${DotnetPluginId}.dll", + "$outputFolder/${DotnetPluginId}.pdb", + // Not 100% sure why, but we manually need to include these dependencies for Remodder to work + "$outputFolder/0Harmony.dll", + "$outputFolder/AsmResolver.dll", + "$outputFolder/AsmResolver.DotNet.dll", + "$outputFolder/AsmResolver.PE.dll", + "$outputFolder/AsmResolver.PE.File.dll", + "$outputFolder/ICSharpCode.Decompiler.dll" + ) + + dllFiles.forEach({ f -> + val file = file(f) + from(file, { into("${rootProject.name}/dotnet") }) + }) + + from("${rootDir}/src/dotnet/${DotnetPluginId}/ProjectTemplates", { into("${rootProject.name}/ProjectTemplates") }) +``` + +This part: +- `dependsOn(compileDotNet)` — ensure `dotnet build` ran before we try to copy the DLLs +- Reads from `bin/ReSharperPlugin.RimworldDev.Rider/Release/` (the Rider flavour; see §12) +- Hand-lists 8 files: the main DLL+PDB plus 6 Remodder runtime dependencies +- `from(file, { into("..../dotnet") })` — Gradle Copy DSL; declares input files and a destination subpath inside the sandbox +- Also copies the `ProjectTemplates/` folder + +**The hand-listed DLLs are a workaround.** IPGP's default sandbox layout knows about the main plugin DLL but not about transitive runtime dependencies. The .NET project sets `true` (in `.Rider.csproj:18`) to dump those DLLs into `bin/`, then this Gradle list re-copies them by name into the sandbox. + +The comment in the code is honest: +> *"Not 100% sure why, but we manually need to include these dependencies for Remodder to work"* + +The "why" is what's described above: the bin/ folder has the DLLs (because of the .csproj flag) but `prepareSandbox` doesn't pick them up automatically (because the Copy DSL only includes what `from(...)` lines explicitly list). + +**Footgun:** if you add a new NuGet package to `.Rider.csproj` whose runtime DLL needs to be in the plugin (rather than provided by Rider), you must add the DLL filename here too. Otherwise the build succeeds but the plugin fails at runtime with a `FileNotFoundException` for the missing assembly. + +### Part 2: existence assertion (`:180-184`) + +```kotlin +doLast { + dllFiles.forEach({ f -> + val file = file(f) + if (!file.exists()) throw RuntimeException("File ${file} does not exist") + }) + // ... continued in part 3 +``` + +A runtime sanity check. If `compileDotNet` succeeded but didn't actually produce one of the expected DLLs (for example, you renamed an excluded source file but forgot to update the exclude rule), the build fails here with a clear message. + +This is technically duplicate work — Gradle's Copy task would itself fail if a `from(file)` source didn't exist. But the explicit check produces a clearer error message, and the failure mode "Configuration changed silently and we now ship a broken plugin" is high-stakes enough to justify defense in depth. + +### Part 3: the DotFiles workaround (`:186-222`) + +The hairiest piece in the build. The comment in the code: + +> *"The Rider SDK archive omits certain DLLs that are present in a full Rider installation. Copy the missing Unity plugin DotFiles DLL from the local Rider installation so the sandbox can load it."* + +What's actually happening: + +The plugin declares `com.intellij.resharper.unity` (`plugin.xml:8`). Rider's Unity plugin ships with native helper DLLs in `plugins/rider-unity/DotFiles/`. These are PE-format binaries the Unity debugger injects into running Unity processes. + +- On **Windows**, Rider's Maven artifact (the `rider:2026.1` distribution IPGP downloads) ships these DLLs. +- On **Mac/Linux**, the Maven artifact is **stripped** of them (presumably to keep platform-specific natives out of a generic JAR). But local Rider installations have them — they're put there by the DMG/tarball installer. + +Without these DLLs, the rider-unity plugin fails to initialize on Mac/Linux when the sandbox boots. And because this plugin `` on rider-unity, our plugin transitively fails to load. + +The hack copies them from a local Rider installation into the sandbox's SDK extraction: + +```kotlin +if (!isWindows) { + val riderInstallCandidates = if (Os.isFamily(Os.FAMILY_MAC)) { + listOf(file("/Applications/Rider.app/Contents")) + } else { + // Linux: check JetBrains Toolbox and common standalone install paths + val toolboxBase = file("${System.getProperty("user.home")}/.local/share/JetBrains/Toolbox/apps/Rider") + val toolboxInstalls = if (toolboxBase.exists()) { + toolboxBase.walkTopDown() + .filter { it.name == "plugins" && it.parentFile?.name?.startsWith("2") == true } + .map { it.parentFile } + .toList() + } else emptyList() + toolboxInstalls + listOf(file("/opt/rider"), file("/usr/share/rider")) + } + + val missingDotFileDlls = listOf( + "JetBrains.ReSharper.Plugins.Unity.Rider.Debugger.PausePoint.Helper.dll", + "JetBrains.ReSharper.Plugins.Unity.Rider.Debugger.Presentation.Texture.dll", + ) + + val destDir = intellijPlatform.platformPath.resolve("plugins/rider-unity/DotFiles").toFile() + destDir.mkdirs() + + for (dllName in missingDotFileDlls) { + val dllRelPath = "plugins/rider-unity/DotFiles/$dllName" + val srcDll = riderInstallCandidates + .map { file("${it}/${dllRelPath}") } + .firstOrNull { it.exists() } + + if (srcDll != null) { + srcDll.copyTo(file("${destDir}/${srcDll.name}"), overwrite = true) + } + } +} +``` + +Three things to call out: + +1. **The destination is the sandbox's *SDK* directory**, not the plugin's own staging. `intellijPlatform.platformPath.resolve("plugins/rider-unity/DotFiles")` writes into where the bundled Unity plugin reads from at runtime. We're patching the SDK in place. + +2. **It silently no-ops if no candidate path matches.** Run `runIde` on a Linux machine without Rider installed and you get no warning, just a sandbox where Unity debugging silently doesn't work. + +3. **It's execution-time only.** All the file walking happens inside `doLast`. Pulling it to configuration time would crash because `intellijPlatform.platformPath` isn't valid until IPGP's initialize task has run. + +**Why this is a "Help wanted from JetBrains" workaround:** the right fix is for JetBrains to either ship those DLLs in the cross-platform Maven artifact, or to have IPGP fetch them on-demand. Until then, the wiki marks this as needing JetBrains involvement (§17, §23). + +## Mental model + +``` + compileDotNet + │ + ▼ + src/dotnet/.../bin/ReSharperPlugin.RimworldDev.Rider/Release/ + │ + prepareSandbox copies named DLLs + │ + ▼ + build/idea-sandbox/plugins/rimworlddev/dotnet/ + │ + Rider plugin loader picks up dotnet/*.dll + │ + ▼ + Backend (Rider.Backend) loads them + +[On Mac/Linux only] + Local Rider install plugins/rider-unity/DotFiles/*.dll + │ + prepareSandbox copies into platformPath + │ + ▼ + build/idea-sandbox/.../platform/plugins/rider-unity/DotFiles/*.dll + │ + Bundled rider-unity plugin loads them +``` + +## What to do when this breaks + +| Symptom | Likely cause | Fix | +|---|---|---| +| `File .../ReSharperPlugin.RimworldDev.dll does not exist` | `compileDotNet` failed silently or didn't run | `./gradlew compileDotNet --info` and read .NET errors | +| `File .../0Harmony.dll does not exist` | NuGet didn't restore, or `` got removed | `dotnet restore`; check `.Rider.csproj:18` | +| `runIde` boots but the plugin fails to load on Linux/Mac | Unity DotFiles missing; install Rider locally via Toolbox | Or, in CI: this currently doesn't bite because CI doesn't run `runIde` | +| New NuGet runtime DLL needed but not bundled | The `dllFiles` list at `build.gradle.kts:160-171` doesn't include it | Add the DLL filename to the list | + +→ Next: [15 · runIde and debugging locally](15-runIde-and-debugging-locally.md) diff --git a/docs/wiki/build-system/part-2-this-project/15-runIde-and-debugging-locally.md b/docs/wiki/build-system/part-2-this-project/15-runIde-and-debugging-locally.md new file mode 100644 index 0000000..f97d5b9 --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/15-runIde-and-debugging-locally.md @@ -0,0 +1,123 @@ +# 15 · runIde and debugging locally + +**[This Project]** — *Mostly the same as IntelliJ plugins, with one twist: you have a second process to attach to.* + +`runIde` is the developer-feedback loop. It builds the plugin, lays out a sandbox (§14), and launches a sandboxed Rider with the plugin pre-installed and a known mod project pre-opened. This page covers using it, attaching debuggers, and reading logs. + +## The basic loop + +```bash +./gradlew runIde +``` + +What happens, in dependency order: + +1. IPGP initialize task — downloads & extracts the Rider SDK on first run (~10 GB; cached afterwards) +2. `compileKotlin` — JVM frontend +3. `compileDotNet` — `dotnet build` for the .NET backend +4. `prepareSandbox` — copies DLLs + JARs + ProjectTemplates into `build/idea-sandbox/plugins/rimworlddev/` +5. `runIde` — launches Rider against the sandbox, pointed at `example-mod/AshAndDust.sln` + +A clean first run takes 5–15 minutes (mostly downloading Rider). Subsequent runs are usually 30–60 seconds — but `compileDotNet` always re-runs (§09 §17), so even no-op cycles aren't instant. + +## What's configured + +`build.gradle.kts:141-154`: + +```kotlin +tasks.runIde { + dependsOn(compileDotNet) + maxHeapSize = "1500m" + autoReload = false + argumentProviders += CommandLineArgumentProvider { + listOf("${rootDir}/example-mod/AshAndDust.sln") + } +} +``` + +- `maxHeapSize = "1500m"` — matches Rider's default. IPGP's default of 512m chokes on real solutions. +- `autoReload = false` — Rider's backend doesn't support dynamic plugin reload. The .NET process can't safely swap code mid-flight, and a desync between the JVM frontend (which can reload) and the .NET backend (which can't) is worse than no reload. Disabled by design. +- The `argumentProviders` lambda passes `example-mod/AshAndDust.sln` to the launched Rider as a CLI argument. Result: Rider auto-opens the example mod's solution, so you have something to test against immediately. + +## Skipping .NET if you only changed Kotlin + +```bash +./gradlew runIde -x compileDotNet +``` + +Half the iteration time. The `-x` flag tells Gradle to exclude a task. Useful when you're only iterating on the JVM half (UI, run configurations, settings). + +## Prerequisites + +For a successful first-time `runIde`: + +- **JDK 21** — Gradle's toolchain auto-provisioning will fetch one if not present, but it's faster to install ahead. +- **.NET SDK 7.0+ on PATH** — `dotnet` must work from your terminal (`global.json` pins 7.0.202 with `rollForward: latestMajor`). +- **~10 GB free disk** — Rider SDK download + sandbox + outputs. +- **Internet on first run** — for the SDK download. + +On **Linux/macOS** additionally: install Rider locally via JetBrains Toolbox (or `/opt/rider`, `/usr/share/rider`, `/Applications/Rider.app`). The `prepareSandbox` DotFiles patcher (§14) copies Unity-debugger DLLs from your local install into the sandbox. Without those DLLs, the bundled rider-unity plugin fails to initialize and our plugin (which `` on it) won't load. Currently no warning is emitted — silent footgun. + +## The two debugger stories + +The plugin runs in two processes, so debugging means choosing which. + +### Frontend (JVM/Kotlin) + +`runIde` runs the launched Rider as a child JVM. Gradle sets up debugging by default — IntelliJ-family IDEs running `runIde` from a Gradle task config provide a "Debug" green-bug button next to the Run button. Click it, breakpoints in `src/rider/main/kotlin/...` work as expected. + +If running `./gradlew runIde` from a terminal, you can attach manually: the JVM is launched with `-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:` style args (controlled by IPGP). Find the port in IDE log output, then "Attach to JVM Process" in your dev IDE. + +### Backend (.NET/C#) + +The launched Rider spawns a separate `Rider.Backend` (or `dotnet/dotnet`) process. To debug the .NET half: + +1. Run `./gradlew runIde` and let the sandbox boot fully. +2. In your **development** Rider (the one you opened the plugin source in), choose *Run → Attach to Process…* +3. Find the `Rider.Backend.exe` (Windows) or `dotnet` (Linux/macOS) process spawned by the sandbox Rider. There may be multiple `dotnet` processes; pick the one with `Rider` in the command line. +4. Attach. Set breakpoints in `src/dotnet/...`. +5. The PDB files copied into the sandbox by `prepareSandbox` provide source mappings. + +This is the same dance as debugging any out-of-process .NET application. There's no Gradle integration for it — JetBrains does not currently provide one. + +## Reading logs + +Sandbox logs live under `build/idea-sandbox/`: + +- **JVM frontend (idea.log)**: `build/idea-sandbox/system/log/idea.log`. The standard IntelliJ Platform log file. Plugin Kotlin code's `Logger.getInstance(...).warn(...)` ends up here. +- **.NET backend (Rider.Backend logs)**: `build/idea-sandbox/system/log/Rider.Backend/log/`. Backend C# logs land here. JetBrains' RD logs and ReSharper plugin logs are mixed in. + +For a fast `tail`: + +```bash +tail -f build/idea-sandbox/system/log/idea.log +``` + +```bash +ls build/idea-sandbox/system/log/Rider.Backend/log/ +# follow the most recent file +``` + +## Common failure modes + +| Symptom | Where to look | +|---|---| +| `runIde` hangs at "Building" | Probably .NET restore. `./gradlew compileDotNet --info` to see what's stuck. | +| Sandbox boots but plugin not loaded | Read `idea.log` for plugin loader errors. Common cause: `` on a plugin/module that's not bundled (e.g. a renamed `intellij.spellchecker`). | +| RD protocol calls hang on the frontend | Backend probably didn't bind. Check the Rider.Backend log for serialization-hash mismatches (means RdFramework versions don't agree). | +| "DotFiles not found" message during sandbox boot | You're on Mac/Linux and don't have Rider installed locally. Install via Toolbox. | +| Plugin loads but XML completion does nothing | `prepareSandbox` may have missed a DLL — check `/dotnet/` in the sandbox tree to confirm all expected DLLs are there. | + +## Useful Gradle invocations + +```bash +./gradlew runIde --dry-run # show task graph without running +./gradlew runIde -i # info-level logging +./gradlew runIde --warning-mode all # surface every deprecation +./gradlew runIde --configuration-cache # try with config cache (will surface cache hostility) +./gradlew :buildPlugin # just produce the ZIP +./gradlew clean # nuke build/ entirely +./gradlew :protocol:rdgen # regenerate protocol bindings +``` + +→ Next: [16 · CI and publishing](16-ci-and-publishing.md) diff --git a/docs/wiki/build-system/part-2-this-project/16-ci-and-publishing.md b/docs/wiki/build-system/part-2-this-project/16-ci-and-publishing.md new file mode 100644 index 0000000..147bc3a --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/16-ci-and-publishing.md @@ -0,0 +1,151 @@ +# 16 · CI and publishing + +**[This Project]** — *Standard GitHub Actions; the publishing flow is dual-artifact (Rider zip + ReSharper nupkg).* + +Two workflows under `.github/workflows/`: + +- `CI.yml` — runs on every push to `main` and every PR. Builds and tests. +- `Deploy.yml` — runs on tag pushes matching `*.*.*`. Publishes to the JetBrains Marketplace and attaches artifacts to a GitHub release. + +Both run on `ubuntu-latest` runners, set up JDK 21 (Corretto, with Gradle cache) and .NET SDK 8, then call Gradle. + +## CI.yml — what runs on every PR + +`.github/workflows/CI.yml`: + +Two jobs, both on `ubuntu-latest`: + +### Build job + +```yaml +- run: ./gradlew :buildPlugin --no-daemon +- run: ./gradlew :buildResharperPlugin --no-daemon +- uses: actions/upload-artifact@v4 + with: + name: ${{ github.event.repository.name }}.CI.${{ github.head_ref || github.ref_name }} + path: output +``` + +This produces: +- `build/distributions/rimworlddev-.zip` — copied to `output/` by the `tasks.buildPlugin { doLast { ... } }` post-action (§09) +- `output/ReSharperPlugin.RimworldDev..nupkg` — produced directly by `buildResharperPlugin`'s `Pack` MSBuild target + +The artifact upload captures everything in `output/` so reviewers can download the built plugin from the GitHub Actions run page. + +`--no-daemon` matters because CI containers benefit from deterministic Gradle shutdown: no daemons left running, no leaked file locks, no surprise behavior on the next job. + +### Test job + +```yaml +- run: ./gradlew :testDotNet --no-daemon +``` + +Currently a near-no-op because `ReSharperPlugin.RimworldDev.Tests` has no `.cs` test files. The runner exits 0 with nothing to fail. When tests are added, this is the gate. + +## Deploy.yml — what runs on a release tag + +`.github/workflows/Deploy.yml`: + +Trigger: push of a tag matching `*.*.*` (e.g. `2025.1.11`). + +Single job with four publish steps: + +### Step 1: Publish the Rider plugin + +```yaml +- name: Publish Rider Package + run: ./gradlew :publishPlugin -PBuildConfiguration="Release" -PPluginVersion="${{ github.ref_name }}" -PPublishToken="${{ secrets.PUBLISH_TOKEN }}" +``` + +This invokes IPGP's `publishPlugin` task. Looking at `build.gradle.kts:232-236`: + +```kotlin +tasks.publishPlugin { + dependsOn(testDotNet) + dependsOn(tasks.buildPlugin) + token.set(PublishToken) +} +``` + +Effective sequence: +1. `testDotNet` runs (currently no-op) +2. `buildPlugin` runs (produces the ZIP) +3. `publishPlugin` uploads the ZIP to JetBrains Marketplace under plugin id `com.jetbrains.rider.plugins.rimworlddev` (from `plugin.xml:2`) + +The `-P` flags override `gradle.properties` defaults. `PluginVersion="${{ github.ref_name }}"` means the tag name itself is the version — so the tag `2025.1.11` becomes the released version. `PublishToken` overrides the `"_PLACEHOLDER_"` default with the real Marketplace token. + +### Step 2: Build the ReSharper nupkg + +```yaml +- run: ./gradlew :buildResharperPlugin +``` + +Produces `output/ReSharperPlugin.RimworldDev..nupkg`. Note: the `:buildResharperPlugin` task in `build.gradle.kts:92-105` doesn't include a `Pack` step that picks up `-PPluginVersion` automatically — but it interpolates `$PluginVersion` from the property at configuration time, so the `-P` override above does flow through. + +### Step 3: Publish the ReSharper plugin + +```yaml +- name: Publish ReSharper Package + run: dotnet nuget push --source "https://plugins.jetbrains.com/api/v2/package" --api-key "$PUBLISH_TOKEN" output/ReSharperPlugin*.nupkg +``` + +This bypasses Gradle entirely — a direct `dotnet nuget push` to JetBrains' Marketplace package endpoint. The Marketplace accepts both `.zip` (Rider/IntelliJ plugins) and `.nupkg` (ReSharper plugins) at different upload paths; this is the ReSharper one. + +The ReSharper plugin lands under id `ReSharperPlugin.RimworldDev` (the assembly name). **Different listing** from the Rider plugin: a Rider user installs `com.jetbrains.rider.plugins.rimworlddev`, a ReSharper-for-VS user installs `ReSharperPlugin.RimworldDev`. They're separately versioned, but typically released in lockstep. + +### Step 4: Attach to GitHub release + +```yaml +- name: Upload binaries to release + run: gh release upload ${{ github.ref_name }} output/* +``` + +Both the `.zip` and the `.nupkg` get attached to the GitHub release for the tag, so users on air-gapped networks (or just looking at the GitHub release page) can grab them directly. + +## What's NOT used by CI + +The PowerShell scripts at the repo root: +- `buildPlugin.ps1` — duplicates `:buildResharperPlugin`, not invoked +- `publishPlugin.ps1` — duplicates the `dotnet nuget push` step, not invoked +- `settings.ps1` — sourced by the above two; not invoked +- `tools/vswhere.exe`, `tools/nuget.exe` — used by `settings.ps1`; not invoked + +Of the four, only **`runVisualStudio.ps1`** has a unique role (set up an experimental ReSharper hive in Visual Studio for local ReSharper-for-VS development). The other three plus the `tools/` directory are deletion candidates (§24). + +## The `-P` property override mechanism + +Throughout `Deploy.yml` you see `-PPluginVersion=...`, `-PPublishToken=...`, `-PBuildConfiguration=...`. These override `gradle.properties` at the CLI level. The flow: + +1. `gradle.properties:7 PluginVersion=2025.1.10` sets a default +2. CI invokes Gradle with `-PPluginVersion="${{ github.ref_name }}"` — Gradle's CLI `-P` flag overrides +3. `val PluginVersion: String by project` in `build.gradle.kts:31` reads the *effective* value (the override) +4. The override propagates to `tasks.patchPluginXml`, `tasks.buildResharperPlugin`, and `tasks.buildPlugin` filenames + +This is how a tag push with `2025.1.11` produces a `rimworlddev-2025.1.11.zip` even though `gradle.properties` still says `2025.1.10`. + +## Summary diagram + +``` + Tag push (e.g. 2025.1.11) + │ + ▼ + Deploy.yml runs + │ + ┌─────────────────┼─────────────────┐ + ▼ ▼ ▼ + publishPlugin buildResharperPlugin GitHub Release + (Gradle) (Gradle) (gh release upload) + │ │ │ + ▼ ▼ │ + rimworlddev ReSharperPlugin │ + -2025.1.11.zip .RimworldDev │ + │ .2025.1.11.nupkg │ + │ │ │ + ▼ ▼ │ + JetBrains JetBrains GitHub Release + Marketplace Marketplace binaries attached + (Rider plugin (ReSharper plugin + listing) listing) +``` + +→ Next: [17 · Quirks and known issues](17-quirks-and-known-issues.md) diff --git a/docs/wiki/build-system/part-2-this-project/17-quirks-and-known-issues.md b/docs/wiki/build-system/part-2-this-project/17-quirks-and-known-issues.md new file mode 100644 index 0000000..b7947c9 --- /dev/null +++ b/docs/wiki/build-system/part-2-this-project/17-quirks-and-known-issues.md @@ -0,0 +1,219 @@ +# 17 · Quirks and known issues + +**[This Project]** — *A ledger of "this looks weird because it IS weird." Each entry is a callout, with file:line and current state.* + +Use this page when you encounter something that doesn't quite make sense — there's a good chance it's listed here. Each entry has a **state** tag: + +- **intentional** — exists for a real reason, leave it +- **workaround** — works around a JetBrains gap; we live with it +- **stale-delete** — dead, should be removed (refactor backlog in §24) +- **drift** — same thing pinned in two places, gradually diverging +- **needs-jetbrains** — root cause is upstream; track via a JetBrains ticket + +## DotFiles patcher (Mac/Linux only) + +**State**: `needs-jetbrains` + `workaround` +**File**: `build.gradle.kts:186-222` + +Copies `JetBrains.ReSharper.Plugins.Unity.Rider.Debugger.PausePoint.Helper.dll` and similar from a local Rider install into the sandbox's SDK directory because the cross-platform Maven artifact strips them. Silently no-ops if no Rider is installed locally — that's the silent footgun. Full discussion in §14. + +The right fix is upstream: either ship those DLLs in the Maven artifact, or let IPGP fetch them on demand. Should have a YouTrack RIDER ticket linked from this section. Until that's resolved, the wiki marks this as **Help wanted from JetBrains** (§23). + +## Hand-rolled Remodder DLL list + +**State**: `workaround` +**File**: `build.gradle.kts:160-171` + +Eight DLL filenames hard-coded into the `prepareSandbox` configuration. If a new NuGet runtime dependency is added to `.Rider.csproj`, you must also add its DLL filename here. The build doesn't enforce this — the failure manifests at plugin load time as a `FileNotFoundException`. + +Refactor candidate: derive the list automatically by globbing `bin/.../*.dll` (or by reading the .csproj's resolved transitive closure). §24. + +## `compileDotNet` etc. have no incrementality + +**State**: `workaround` +**Files**: `build.gradle.kts:86-90, :92-105, :226-230` + +The `Exec`-based .NET tasks (`compileDotNet`, `buildResharperPlugin`, `testDotNet`) declare no `@InputDirectory` / `@OutputDirectory`. Gradle has no idea what's an input or output. Consequences: + +- They re-run on every build, even when no `.cs` file changed +- They're incompatible with Gradle's build cache +- Configuration cache support is degraded + +`dependsOn(compileDotNet)` orders dependent tasks correctly but doesn't make `compileDotNet` skippable. Refactor candidate: convert to a typed task class in `buildSrc/` with proper inputs/outputs declared. §24. + +## `apply { plugin("kotlin") }` is redundant + +**State**: `stale-delete` +**File**: `build.gradle.kts:53-55` + +The Kotlin JVM plugin was already applied via `alias(libs.plugins.kotlinJvm)` at line 8. Dead code from a template ancestor. Safe to delete. + +## `riderBaseVersion=2025.1` + +**State**: `stale-delete` +**File**: `gradle.properties:28` + +Zero references in the entire codebase (Kotlin, Gradle, PowerShell, .NET). Safe to delete. + +## Version drift between gradle.properties and build.gradle.kts + +**State**: `drift` +**Files**: `gradle.properties:26-27` ↔ `build.gradle.kts:9-10` + +Two plugins are pinned in both places: + +| Plugin | gradle.properties | build.gradle.kts | Status | +|---|---|---|---| +| `intellijPlatformGradlePluginVersion` | 2.14.0 | 2.14.0 | currently synced | +| `gradleJvmWrapperVersion` | 0.15.0 | 0.16.0 | **drifted** — `gradle.properties` value is dead | + +The inline declaration in `build.gradle.kts` wins. The `gradle.properties` value gets read by `pluginManagement` in `settings.gradle.kts` for the *default* version — but the root build script then overrides with its own. Net effect: `gradle.properties` for these is misleading. §07's table flags this; §24 has the consolidation refactor. + +## `.run/Build Plugin.run.xml` references stale JDK + +**State**: `drift` +**File**: `.run/Build Plugin.run.xml:8` + +References `corretto-17.0.7` while the project's toolchain is JDK 21. Importing this run config in IntelliJ either silently downgrades or fails. Should be updated to `corretto-21` or have the env var removed entirely (let the toolchain handle it). + +## `.run/Build ReSharper Plugin.run.xml` invokes legacy PowerShell + +**State**: `stale-delete` +**File**: `.run/Build ReSharper Plugin.run.xml` + +Calls `buildPlugin.ps1`, which is a legacy script not used by CI. Should be replaced with a Gradle run config calling `:buildResharperPlugin`. Together with deleting the `.ps1` files, this is §24.3. + +## `${buildDir}` is deprecated in Gradle 9, removed in 10 + +**State**: `workaround` (will become `breakage` on Gradle 10) +**File**: `build.gradle.kts:110` + +Gradle deprecated `${buildDir}` (string-interpolated property) in favour of `layout.buildDirectory.dir(...)` (lazy `Provider`). Used at `:110` inside `tasks.buildPlugin { doLast { copy { from("${buildDir}/distributions/...") ... } } }`. Will warn loudly with `--warning-mode all`; bumps to Gradle 10 will break it. + +The fix is straightforward: + +```kotlin +val pluginZip = layout.buildDirectory.file("distributions/${rootProject.name}-${version}.zip") +val outputDir = layout.projectDirectory.dir("output").asFile + +tasks.buildPlugin { + doLast { + val zipFile = pluginZip.get().asFile + outputDir.mkdirs() + zipFile.copyTo(outputDir.resolve(zipFile.name), overwrite = true) + } +} +``` + +§24 has this captured. + +## `tasks.patchPluginXml` reads CHANGELOG.md eagerly + +**State**: `workaround` +**File**: `build.gradle.kts:239-247` + +The `file("${rootDir}/CHANGELOG.md").readText()` call runs at *configuration time* — every Gradle invocation reads the file, even no-op ones. Wrap in `provider { }` for laziness: + +```kotlin +changeNotes.set(provider { + val changelogText = file("${rootDir}/CHANGELOG.md").readText() + .lines() + // ... existing parse ... + "
      \r\n$changelogText\r\n
    " +}) +``` + +Captured in §24. + +## `untilBuild = null` + +**State**: `intentional`, with risk +**File**: `build.gradle.kts:251` + +`untilBuild.set(provider { null })` clears IPGP's auto-computed upper-bound on Rider compatibility. Default is to set `until-build` to the same major.minor as the SDK, blocking the plugin from loading on EAPs of the next version. + +The maintainer's choice: opt for "always loadable" because (a) this plugin rarely hits binary breakage, (b) blocking users on legitimate upgrades is annoying, (c) when JetBrains does break something, the maintainer would rather get bug reports than have users blocked. + +The trade-off: when JetBrains ships a Rider that breaks the plugin's API surface, *users hit it* instead of being told the plugin is incompatible. Risk-acceptance, not negligence. + +## Empty Tests project + zero Kotlin tests + +**State**: `gap` +**Files**: `src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj` (no .cs files), no `src/rider/test/` directory + +Test infrastructure exists in skeleton form but no tests are wired up: +- `Tests.csproj` references `Lib.Harmony` and the main plugin csproj but contains zero `.cs` files. Only `test/data/nuget.config` exists. +- No Kotlin frontend tests. No `BasePlatformTestCase` / `RiderTestBase` references in the codebase. +- The `testDotNet` Gradle task and `:publishPlugin` gate exist; on this stub they're effectively no-ops. + +The `example-mod/` directory is a real fixture mod loaded by `runIde` as a manual fixture. Convertible to an integration test fixture with effort. The maintainer's stated goal — see §24.12. + +## PowerShell scripts (mostly legacy) + +**State**: mostly `stale-delete` +**Files**: `buildPlugin.ps1`, `publishPlugin.ps1`, `settings.ps1`, `tools/vswhere.exe`, `tools/nuget.exe` + +Inherited from a JetBrains template. **Not used by CI.** They duplicate functionality already provided by Gradle (`:buildResharperPlugin`, the `dotnet nuget push` step in `Deploy.yml`). Deletion candidates. + +The exception is `runVisualStudio.ps1` — it sets up an experimental ReSharper hive inside Visual Studio for local ReSharper-for-VS development. That's a real use case Gradle doesn't cover. Decision deferred (§24.3 leaves it to the maintainer). + +## Three `intellijPlatform { }` scopes in one file + +**State**: `intentional` +**Files**: `build.gradle.kts:47-50` (inside `repositories`), `:117-127` (inside `dependencies`), `:130-139` (top-level) + +Each is the IPGP DSL scoped to a different container: +- Inside `repositories { }`: configure where to fetch the SDK from +- Inside `dependencies { }`: declare the SDK and bundled-plugin dependencies +- Top-level: configure plugin-wide settings (verifier, etc.) + +They look like duplicates but configure different things. Not a smell. + +## `useInstaller = false` and `useBinaryReleases = false` + +**State**: `intentional`, tightly coupled +**Files**: `build.gradle.kts:120` and `gradle.properties:31` + +Together they tell IPGP: download the Rider SDK as Maven artifacts (JARs in `intellij-repository/releases`), not as a binary installer/CDN tarball. The Maven path: +- Plays nicely with `actions/setup-java cache: gradle` in CI +- Works through JetBrains' cache-redirector mirrors +- Gives an extracted layout that this build's `riderModel` configuration can read from + +Don't flip one without the other; they're coupled via the artifact path expectations downstream. + +## `useBinaryReleases` lives in gradle.properties as a `buildFeature` flag + +**State**: `intentional`, may rename +**File**: `gradle.properties:31` + +`org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false` is an IPGP buildFeature flag. These flags can be renamed/removed across IPGP versions. If you upgrade IPGP and the build dies with "could not resolve com.jetbrains.intellij.rider:rider:2026.1", check the IPGP changelog for renames of this flag. + +## `kotlin.stdlib.default.dependency=false` + +**State**: `intentional`, do not change +**File**: `gradle.properties:21` + +The IDE bundles a Kotlin stdlib at runtime. If Gradle pulls in another (because the Kotlin plugin auto-adds one by default), you get duplicate-class loader fights at plugin load time. This setting suppresses the auto-add. Documented in the file itself with a link to Kotlin 1.4 release notes. + +## `org.gradle.jvmargs=-Xmx4g` + +**State**: `intentional`, do not lower +**File**: `gradle.properties:22` + +IPGP sandbox extraction is memory-hungry. Below 4 GB you'll OOM on the SDK extraction step. Don't lower without testing. + +## `instrumentCode/instrumentTestCode disabled` + +**State**: `intentional` +**File**: `build.gradle.kts:74-80` + +Disables IPGP's NotNull annotation instrumentation. Historical: instrumentation used to choke on rdgen-generated bytecode and on plugins without `.form` files. This plugin has neither problem, but the disable is harmless and avoids a class of CI flakes. + +## `riderModel` configuration arcanity + +**State**: `intentional`, canonical JetBrains +**File**: `build.gradle.kts:254-269`, `protocol/build.gradle.kts:11-18` + +The custom Configuration / artifacts / `builtBy(INITIALIZE_INTELLIJ_PLATFORM_PLUGIN)` dance is canonical JetBrains plugin pattern. See §13. Not a smell, but obscure if you've never seen it. + +→ End of Part 2. Next: [18 · Recipes](../part-3-operate/18-recipes.md) diff --git a/docs/wiki/build-system/part-3-operate/18-recipes.md b/docs/wiki/build-system/part-3-operate/18-recipes.md new file mode 100644 index 0000000..06937f8 --- /dev/null +++ b/docs/wiki/build-system/part-3-operate/18-recipes.md @@ -0,0 +1,178 @@ +# 18 · Recipes + +**[Operate / Recipes]** — *Day-to-day "how do I do X?" answers. Bookmark this page.* + +Each recipe is short and copy-pasteable. If a recipe needs context, it links back to the Part 2 page that explains why. + +## Run the plugin locally + +```bash +./gradlew runIde +``` + +Prerequisites: +- JDK 21 on PATH (Gradle's toolchain will auto-provision if missing) +- .NET SDK 7+ on PATH (`global.json` pins 7.0.202 with `rollForward: latestMajor`) +- ~10 GB free disk (Rider SDK + sandbox) +- Internet on first run (SDK download) +- **On Mac/Linux**: Rider installed locally via JetBrains Toolbox, `/opt/rider`, `/usr/share/rider`, or `/Applications/Rider.app`. Otherwise the DotFiles patcher silently no-ops and the rider-unity plugin fails to load. See §14. + +A clean first run takes 5–15 minutes (mostly Rider SDK download). Subsequent runs ~30–60 seconds. + +## Iterate on Kotlin only (skip .NET rebuild) + +```bash +./gradlew runIde -x compileDotNet +``` + +Half the iteration time. The `-x` flag tells Gradle to exclude a task. Valid as long as you haven't touched `.cs` files. + +## Bump the plugin version + +1. Edit `gradle.properties:7`: + ```properties + PluginVersion=2025.1.11 + ``` +2. Add a new `## 2025.1.11` section at the **top** of `CHANGELOG.md`. The `tasks.patchPluginXml` block parses the first `##`-headed section into the plugin's "what's new" HTML. +3. (Optional) The `` in `plugin.xml` is overridden by `patchPluginXml` at build time, so you don't need to edit it. + +Test: `./gradlew :buildPlugin` — output filename should reflect the new version. + +## Add a .NET package reference + +1. Edit `src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.Rider.csproj` (the Rider flavour, where Remodder dependencies live): + ```xml + + ``` +2. If the package needs to be in the Wave/ReSharper flavour too, edit `ReSharperPlugin.RimworldDev.csproj` instead (or in addition). +3. If the package ships a runtime DLL that must end up in the plugin (rather than provided by Rider), add the DLL filename to `prepareSandbox`'s `dllFiles` list at `build.gradle.kts:160-171`: + ```kotlin + "$outputFolder/YourPackage.dll", + ``` +4. Test: `./gradlew compileDotNet` to verify the .csproj builds, then `./gradlew :buildPlugin` to verify the DLL ends up in the plugin ZIP. + +(Yes, manually adding the DLL filename is fragile. §17 + §24 capture this.) + +## Add a new RPC between frontend and backend + +1. Edit `protocol/src/main/kotlin/model/rider/Model.kt`. Add a `call(...)`, `signal(...)`, or `property(...)` inside `init { }`: + ```kotlin + call("doThing", string, int).async + ``` +2. Run `./gradlew :protocol:rdgen`. +3. Verify the regenerated files appeared with new symbols: + - `src/rider/main/kotlin/remodder/RemodderProtocolModel.Generated.kt` + - `src/dotnet/ReSharperPlugin.RimworldDev/RemodderProtocolModel.Generated.cs` +4. Implement the call site (frontend Kotlin) and the handler (backend C#). +5. Commit the regenerated `*.Generated.*` files alongside your protocol edit. + +See §10 for how rdgen and the protocol DSL work. + +## Bump the Rider SDK (e.g. 2026.1 → 2026.2) + +This requires editing several places. See §19 for the full runbook. Quick version: edit `gradle.properties` (`ProductVersion`, `rdVersion`), `Directory.Build.props` (`SdkVersion`), `gradle/libs.versions.toml` (`rdGen` patch). Possibly also `rdKotlinVersion` and `kotlin` if the bundled Kotlin shifts. + +## Publish a release + +1. Make sure `gradle.properties:7 PluginVersion` matches the version you're about to tag (cosmetic — `Deploy.yml` overrides it via `-PPluginVersion="${{ github.ref_name }}"`). +2. Update `CHANGELOG.md` with the new section. +3. Push a Git tag matching `*.*.*`: + ```bash + git tag 2025.1.11 + git push origin 2025.1.11 + ``` +4. `Deploy.yml` runs automatically: publishes the Rider plugin to Marketplace, the ReSharper plugin to Marketplace, and attaches both to a GitHub release. + +Prerequisites: +- The `PUBLISH_TOKEN` repo secret must be set with a valid JetBrains Marketplace token. + +## Run the .NET tests + +```bash +./gradlew testDotNet +``` + +Currently a near-no-op (the Tests project has no `.cs` files yet). When tests are added, this is the gate that runs them. + +Equivalent direct invocation: `dotnet test ReSharperPlugin.RimworldDev.sln`. + +## Build the ReSharper-only nupkg locally + +```bash +./gradlew buildResharperPlugin +``` + +Output lands at `output/ReSharperPlugin.RimworldDev..nupkg`. Useful for testing the ReSharper-for-VS flavour without going through the publish flow. + +## Skip the IntelliJ plugin verifier + +It only runs on `:verifyPlugin`, which CI doesn't currently invoke. Don't run it. (To run it: `./gradlew verifyPlugin`.) + +## Override a Gradle property at the command line + +```bash +./gradlew buildPlugin -PPluginVersion=2025.1.99 +``` + +The `-P=` flag overrides any `gradle.properties` entry of the same name. Used by CI and by anyone who needs to test with a non-default value without committing. + +## Connect a debugger to the running plugin + +See §15 for the full procedure. + +Short version: +- **JVM frontend**: click the Debug button next to `runIde` in your dev IDE, or attach manually to the JVM process spawned by `./gradlew runIde`. +- **.NET backend**: in your dev Rider, *Run → Attach to Process…*, find the spawned `Rider.Backend` (or `dotnet`) process, attach. Set breakpoints in `src/dotnet/...`. + +## Diagnose "File … does not exist" from prepareSandbox + +The `prepareSandbox.doLast` block asserts each expected DLL exists in `bin/`. If the assertion fires: + +1. Did `compileDotNet` actually run? `./gradlew compileDotNet --info` and check the output. +2. Is the DLL in the right `bin/` folder? Check `src/dotnet/ReSharperPlugin.RimworldDev/bin/ReSharperPlugin.RimworldDev.Rider/Release/`. +3. If the DLL is missing entirely, check whether the .csproj still includes it (e.g. did you remove a `PackageReference` recently?). +4. If the DLL is in a different folder, has the `BuildConfiguration` property changed? + +See §14 for the full troubleshooting matrix. + +## Regenerate the protocol bindings without building everything + +```bash +./gradlew :protocol:rdgen +``` + +Runs only the rdgen task on the `:protocol` subproject. The regenerated `*.Generated.kt` and `*.Generated.cs` will appear; `git diff` will show what changed. + +## Clean everything + +```bash +./gradlew clean +``` + +Wipes `build/` directories. Doesn't touch the cached IDE downloads (those live in `~/.gradle/`) or `output/`. Use `git clean -fdx` if you need to nuke `output/` and other gitignored content too — be careful. + +## Inspect the task graph + +```bash +./gradlew runIde --dry-run +``` + +Prints what would run without doing it. Useful when you suspect a `dependsOn` is missing or wrong. + +```bash +./gradlew :buildPlugin --info +./gradlew :buildPlugin --warning-mode all +./gradlew :buildPlugin --configuration-cache +``` + +Increasingly verbose modes for debugging build behaviour. `--configuration-cache` is the most stringent and surfaces any cache-hostile patterns. + +## "I just want a release ZIP" + +```bash +./gradlew :buildPlugin +``` + +Produces `build/distributions/rimworlddev-.zip` and copies it to `output/` (the `tasks.buildPlugin { doLast }` post-action handles this). + +→ Next: [19 · Upgrade runbooks](19-upgrade-runbooks.md) diff --git a/docs/wiki/build-system/part-3-operate/19-upgrade-runbooks.md b/docs/wiki/build-system/part-3-operate/19-upgrade-runbooks.md new file mode 100644 index 0000000..6bee65b --- /dev/null +++ b/docs/wiki/build-system/part-3-operate/19-upgrade-runbooks.md @@ -0,0 +1,192 @@ +# 19 · Upgrade runbooks + +**[Operate / Recipes]** — *Precise edit lists for each common upgrade. Cross-reference §07 (version map) and §17 (known issues).* + +Each runbook lists the files to edit, the order to do it in, and the verification step. Where two files are listed, edit BOTH unless the version is consolidated (which it isn't yet — see §24). + +## Gradle wrapper bump + +E.g. 9.4.1 → 10.0. + +1. Edit `gradle/wrapper/gradle-wrapper.properties:3`: + ```properties + distributionUrl=https\://cache-redirector.jetbrains.com/services.gradle.org/distributions/gradle-10.0-all.zip + ``` +2. Edit `build.gradle.kts:58`: + ```kotlin + gradleVersion = "10.0" + ``` +3. Run the canonical Gradle wrapper update sequence (twice — this is a real Gradle quirk): + ```bash + ./gradlew wrapper + ./gradlew wrapper + ``` +4. Verify with deprecations enabled: + ```bash + ./gradlew :buildPlugin --warning-mode all --configuration-cache --build-cache + ``` +5. **Mandatory for Gradle 10**: replace `${buildDir}` (deprecated in 9, removed in 10): + - `build.gradle.kts:110` — `from("${buildDir}/distributions/...")` → `layout.buildDirectory.file("distributions/...")` +6. Compatibility matrix: + +Reference compatibility: +- Gradle 9.x supports JDK 17, 21 +- Gradle 10.x supports JDK 21+ +- Kotlin Gradle plugin must be compatible — consult its release notes + +## IntelliJ Platform Gradle Plugin (IPGP) bump + +E.g. 2.14.0 → 2.15.0. + +1. Edit `gradle.properties:26`: + ```properties + intellijPlatformGradlePluginVersion=2.15.0 + ``` +2. Edit `build.gradle.kts:9`: + ```kotlin + id("org.jetbrains.intellij.platform") version "2.15.0" + ``` +3. The two locations must agree (until §24's consolidation refactor lands). +4. Verify: + ```bash + ./gradlew :buildPlugin :verifyPlugin + ``` +5. Watch for changes to: + - The `intellijPlatform { ... }` extension shape (esp. `pluginVerification.ides` API) + - Renamed `buildFeature` flags (`useBinaryReleases` is one such flag — see §17) + - `bundledPlugin` vs `bundledModule` reclassifications + +Changelog: + +## rdgen / Rider SDK bump + +E.g. Rider 2026.1 → 2026.2. This is the *biggest* upgrade because it touches both Gradle and .NET. + +1. Edit `gradle.properties:17`: + ```properties + ProductVersion=2026.2 + ``` +2. Edit `gradle.properties:24`: + ```properties + rdVersion=2026.2 + ``` +3. Edit `Directory.Build.props:4`: + ```xml + 2026.2.* + ``` +4. Edit `gradle/libs.versions.toml:3` to the corresponding `rd-gen` patch: + ```toml + rdGen = "2026.2.X" # see https://github.com/JetBrains/rd/releases for the exact patch + ``` +5. **Verify the Kotlin pin** (`gradle/libs.versions.toml:2 kotlin` and `gradle.properties:25 rdKotlinVersion`) against the Kotlin compatibility matrix: + + If the bundled Kotlin in the new Rider differs, bump these too. +6. Run rdgen to regenerate bindings (in case the Rider model changed): + ```bash + ./gradlew :protocol:rdgen + ``` +7. Check that the regenerated `*.Generated.kt` and `*.Generated.cs` still compile against your hand-written code: + ```bash + ./gradlew :buildPlugin + ./gradlew compileDotNet + ``` +8. Test in a sandbox: + ```bash + ./gradlew runIde + ``` +9. Update `plugin.xml:6 since-build` to the matching IDE build number if you want to formally signal compatibility (the build number for 2026.2 would be `262`). + +Reference compatibility: +- `rdGen` major.minor typically matches `Rider` major.minor +- `rdGen` patch can be bumped independently for bug fixes +- `JetBrains.RdFramework` and `JetBrains.Lifetimes` (in `Directory.Build.props`) are pinned to `$(SdkVersion)` and follow + +Changelog: + +## Kotlin bump + +E.g. 2.3.20 → 2.4.0. + +1. Edit `gradle/libs.versions.toml:2`: + ```toml + kotlin = "2.4.0" + ``` +2. Edit `gradle.properties:25` (the version used inside `pluginManagement`): + ```properties + rdKotlinVersion=2.4.0 + ``` +3. Verify against the Kotlin ↔ IntelliJ Platform matrix: + + The bundled Kotlin in the IDE must support compiling against your version. +4. Test: + ```bash + ./gradlew :buildPlugin + ``` + +## JDK bump + +E.g. 21 → 22. + +Five places, all need to agree: + +1. `build.gradle.kts:14-20` — toolchain block (`JavaLanguageVersion.of(22)`) +2. `build.gradle.kts:82-84` — Kotlin's `JvmTarget` (`JvmTarget.JVM_22`) +3. `.github/workflows/CI.yml:21` — `java-version: '22'` +4. `.github/workflows/Deploy.yml:21` — `java-version: '22'` +5. `.run/Build Plugin.run.xml:8` — JDK reference (currently stale at `corretto-17.0.7` — fix to 21 first as part of any toolchain work) + +Compatibility: +- The IntelliJ Platform itself ships built for a specific JDK. Don't go above what the platform supports — consult the IPGP changelog and Rider release notes. +- Gradle and Kotlin Gradle Plugin must support the JDK; consult . + +## .NET SDK bump + +E.g. 7.0.202 → 8.0.x. + +1. Edit `global.json:3-5`: + ```json + { + "sdk": { + "version": "8.0.100", + "rollForward": "latestMajor", + "allowPrerelease": true + } + } + ``` +2. Verify against the Rider host TFM. The plugin currently targets `net6.0` (in both csprojs at line 11). Bumping the SDK is generally backwards-compatible with old TFMs; bumping the *target framework* requires Rider to support it (consult JetBrains). +3. CI's `actions/setup-dotnet` versions are usually broader (`8.0.x`) and don't need updating unless you go major. + +## Adding a new bundled-plugin or bundled-module dependency + +1. Edit `build.gradle.kts:117-127` `dependencies.intellijPlatform { }` block: + ```kotlin + bundledPlugin("com.example.plugin") + // or + bundledModule("intellij.example") + ``` +2. Edit `src/rider/main/resources/META-INF/plugin.xml` to declare the runtime dependency: + ```xml + com.example.plugin + ``` +3. Test: `./gradlew runIde` — verify the launched Rider has the dependency loaded and your plugin loads. + +Watch out: bundled plugin vs. bundled module categorization can shift between Platform versions. If `bundledPlugin("...")` errors with "not found," try `bundledModule("...")` and consult the IPGP changelog. + +## Common mistakes to avoid during an upgrade + +- **Bumping `gradle.properties` but not `build.gradle.kts`** (or vice versa) for IPGP / jvm-wrapper. Both must agree until §24's consolidation refactor. +- **Forgetting to run `:protocol:rdgen` after a Rider SDK bump.** The model is checked in, but it's against the *old* SDK. New SDK may add fields that shift hashes. +- **Not running with `--warning-mode all` after an upgrade.** Deprecations get loud one version before they break. Reading them gives you advance warning. +- **Using `--configuration-cache` only after the upgrade is "done."** Run with it the whole time; cache hostility surfaces as a config-time error during cache write, which is the easiest place to fix it. +- **Ignoring `verifyPlugin` failures.** They catch real binary-incompat issues. Run `./gradlew verifyPlugin` after any IDE-side bump. + +## Compatibility-matrix anchor URLs (one-stop lookup) + +- **Gradle ↔ JDK ↔ Kotlin Gradle Plugin**: +- **Kotlin ↔ IntelliJ Platform**: +- **rd / rdgen releases**: +- **IPGP releases / changelog**: +- **Rider build numbers ↔ release names**: +- **JetBrains Plugin Verifier**: + +→ End of Part 3. Next: [20 · Task graph diagrams](../part-4-reference/20-task-graph-diagrams.md) diff --git a/docs/wiki/build-system/part-4-reference/20-task-graph-diagrams.md b/docs/wiki/build-system/part-4-reference/20-task-graph-diagrams.md new file mode 100644 index 0000000..ebaa7bb --- /dev/null +++ b/docs/wiki/build-system/part-4-reference/20-task-graph-diagrams.md @@ -0,0 +1,171 @@ +# 20 · Task graph diagrams + +**[Reference]** — *Visual reference for how tasks chain together. Cross-reference §09 (annotated build), §14 (prepareSandbox), §16 (CI).* + +## Task DAG (the bare essentials) + +```mermaid +graph LR + initSdk[initializeIntelliJPlatformPlugin
    downloads + extracts Rider SDK] + rdgen[":protocol:rdgen
    Model.kt → .Generated.kt + .Generated.cs"] + compileDotNet[compileDotNet
    dotnet build] + compileKotlin[compileKotlin] + prepareSandbox[prepareSandbox
    copy DLLs + JARs into fake plugins dir] + runIde[runIde
    launch Rider against sandbox] + buildPlugin[buildPlugin
    zip the sandbox] + patchPluginXml[patchPluginXml
    inject version + changelog] + publishPlugin[publishPlugin
    upload to Marketplace] + testDotNet[testDotNet
    dotnet test] + buildResharper[buildResharperPlugin
    dotnet msbuild Pack] + + initSdk --> compileKotlin + initSdk --> rdgen + rdgen -.manual.-> compileKotlin + rdgen -.manual.-> compileDotNet + compileKotlin --> patchPluginXml + patchPluginXml --> prepareSandbox + compileDotNet --> prepareSandbox + prepareSandbox --> runIde + prepareSandbox --> buildPlugin + testDotNet --> publishPlugin + buildPlugin --> publishPlugin + buildResharper -.standalone.-> nupkg[output/*.nupkg] +``` + +Notes: +- `rdgen` is shown with dashed `-.manual.-` edges because it's NOT in the build path of `:buildPlugin` / `compileKotlin`. The arrow indicates that *if you run rdgen, the regenerated files become inputs to compileKotlin/compileDotNet.* But the build doesn't trigger rdgen automatically. +- `buildResharperPlugin` is standalone — only invoked when explicitly requested or by `Deploy.yml`. +- `compileDotNet` re-runs every build (no `@OutputFiles` declared). The arrow into `prepareSandbox` is a `dependsOn`, not an incremental input/output relationship. + +To verify against your local build: + +```bash +./gradlew runIde --dry-run +./gradlew buildPlugin --dry-run +./gradlew publishPlugin --dry-run +``` + +If the diagram diverges from `--dry-run` output, update the diagram (it's authored at a point in time; reality is the source of truth). + +## File flow (where bytes physically move) + +```mermaid +graph TB + src_cs["src/dotnet/...*.cs
    (C# sources)"] + src_kt["src/rider/main/kotlin/...*.kt
    (Kotlin sources)"] + model["protocol/.../Model.kt
    (rdgen DSL)"] + + gen_cs[src/dotnet/.../*.Generated.cs] + gen_kt[src/rider/main/kotlin/remodder/*.Generated.kt] + + dll["bin/.../ReSharperPlugin.RimworldDev.dll
    + 0Harmony.dll, AsmResolver*.dll, etc."] + jar[build/libs/rimworlddev-X.Y.Z.jar] + + sandbox[build/idea-sandbox/plugins/rimworlddev/
    +-- lib/ jars
    +-- dotnet/ dlls
    +-- ProjectTemplates/] + + zip[output/rimworlddev-X.Y.Z.zip] + nupkg[output/ReSharperPlugin.RimworldDev.X.Y.Z.nupkg] + rider[Rider IDE running the sandbox] + + model -->|rdgen| gen_cs + model -->|rdgen| gen_kt + src_cs -->|dotnet build| dll + gen_cs --> src_cs + gen_kt --> src_kt + src_kt -->|compileKotlin + jar| jar + dll -->|prepareSandbox copy| sandbox + jar -->|prepareSandbox copy| sandbox + sandbox -->|buildPlugin zips| zip + sandbox -->|runIde loads| rider + src_cs -->|dotnet msbuild Pack| nupkg +``` + +## Version-pinning map (which file controls which artifact) + +```mermaid +graph LR + gp[gradle.properties] + tom[gradle/libs.versions.toml] + dbp[Directory.Build.props] + bgk[build.gradle.kts inline] + gjs[global.json] + + gp -->|PluginVersion| pluginzip[Plugin zip filename + plugin.xml version] + gp -->|ProductVersion| ridersdk[Rider SDK download via IPGP] + gp -->|rdVersion| rdgenplugin[com.jetbrains.rdgen Gradle plugin] + gp -->|rdKotlinVersion| pmkotlin[Kotlin in pluginManagement] + gp -.DUP.-> ipgp + bgk -->|inline declaration| ipgp[IntelliJ Platform Gradle Plugin] + gp -.DUP DRIFTED.-> wrap + bgk -->|inline declaration| wrap[gradle-jvm-wrapper plugin] + tom -->|kotlin| kotlinplugin[Kotlin Gradle plugin and stdlib] + tom -->|rdGen| rdgenlib[rd-gen library in :protocol] + dbp -->|SdkVersion| nugetpkgs[JetBrains.Lifetimes / RdFramework / Rider.SDK NuGets] + dbp -->|WaveVersion computed| wavepkg[Wave NuGet for ReSharper-for-VS] + gjs -->|sdk.version| dotnet[.NET SDK that runs dotnet build] +``` + +The dashed `DUP` and `DUP DRIFTED` edges mark places where the same property is pinned in two files. §07 has the full table. + +## Sandbox layout (what `prepareSandbox` produces) + +``` +build/idea-sandbox/ +├── config/ ← IDE settings +├── system/ +│ └── log/ +│ ├── idea.log ← JVM frontend logs +│ └── Rider.Backend/log/ ← .NET backend logs +└── plugins/ + └── rimworlddev/ ← OUR PLUGIN + ├── lib/ + │ ├── rimworlddev-X.Y.Z.jar ← compiled Kotlin + │ └── (transitive JARs) + ├── dotnet/ ← LOAD-BEARING; Rider sweeps this for *.dll + │ ├── ReSharperPlugin.RimworldDev.dll + │ ├── ReSharperPlugin.RimworldDev.pdb + │ ├── 0Harmony.dll + │ ├── AsmResolver.dll + │ ├── AsmResolver.DotNet.dll + │ ├── AsmResolver.PE.dll + │ ├── AsmResolver.PE.File.dll + │ └── ICSharpCode.Decompiler.dll + ├── ProjectTemplates/ + │ └── RimworldProjectTemplate/ + │ └── ... + └── META-INF/ + └── plugin.xml ← patched at build by patchPluginXml +``` + +## Distribution flow (CI publish) + +```mermaid +graph TB + tag[Tag push e.g. 2025.1.11] + deploy[Deploy.yml runs] + publishGr[gradle :publishPlugin] + buildRsh[gradle :buildResharperPlugin] + nugetPush[dotnet nuget push] + ghRel[gh release upload] + + rZip[output/rimworlddev-2025.1.11.zip] + rNupkg[output/ReSharperPlugin.RimworldDev.2025.1.11.nupkg] + + mpRider[JetBrains Marketplace
    com.jetbrains.rider.plugins.rimworlddev] + mpReSharper[JetBrains Marketplace
    ReSharperPlugin.RimworldDev] + ghAssets[GitHub Release assets] + + tag --> deploy + deploy --> publishGr + deploy --> buildRsh + publishGr --> rZip + buildRsh --> rNupkg + rZip --> mpRider + rNupkg --> nugetPush + nugetPush --> mpReSharper + rZip --> ghRel + rNupkg --> ghRel + ghRel --> ghAssets +``` + +→ Next: [21 · Contributed tasks table](21-contributed-tasks-table.md) diff --git a/docs/wiki/build-system/part-4-reference/21-contributed-tasks-table.md b/docs/wiki/build-system/part-4-reference/21-contributed-tasks-table.md new file mode 100644 index 0000000..d2eb4eb --- /dev/null +++ b/docs/wiki/build-system/part-4-reference/21-contributed-tasks-table.md @@ -0,0 +1,81 @@ +# 21 · Contributed tasks table + +**[Reference]** — *Every Gradle task this build exposes, who contributes it, what it does, and whether it's incremental.* + +When you see `tasks.somethingYouNeverDefined { ... }` in `build.gradle.kts`, this table tells you who added it. + +## IPGP-contributed (IntelliJ Platform Gradle Plugin) + +The bulk of the build's "it just works" surface area. + +| Task | Purpose | Inputs/Outputs | Incremental? | +|---|---|---|---| +| `initializeIntelliJPlatformPlugin` | Download + extract the Rider SDK | network → `~/.gradle/caches/intellij-platform/...` | Yes (cached) | +| `setupDependencies` | Wire IDE deps onto compile classpath | (transparent) | Yes | +| `prepareSandbox` | Lay out plugin files into `build/idea-sandbox/plugins//` | `from(...)` files → sandbox dir | Yes (Copy task) but `doLast` actions are not | +| `runIde` | Launch sandboxed Rider | sandbox + JBR | N/A (always runs) | +| `buildPlugin` | Zip the sandbox into a distributable plugin ZIP | sandbox → `build/distributions/-.zip` | Yes | +| `verifyPlugin` | Run JetBrains' Plugin Verifier across IDE versions | plugin ZIP + IDE versions → verifier reports | Yes | +| `publishPlugin` | Upload plugin ZIP to JetBrains Marketplace | plugin ZIP + token → Marketplace | N/A (network) | +| `patchPluginXml` | Rewrite `plugin.xml` with version, change notes, build range | plugin.xml → patched plugin.xml | Yes | +| `instrumentCode` | NotNull annotation bytecode instrumentation | classes → instrumented classes | **DISABLED** in this build (`build.gradle.kts:74-76`) | +| `instrumentTestCode` | Same, for test classes | (transparent) | **DISABLED** in this build (`:78-80`) | +| `printBundledPlugins` | Diagnostic: list bundled plugins available in the configured SDK | (transparent) | N/A | +| `printProductsReleases` | Diagnostic: list known IDE releases | (transparent) | N/A | + +Configured in `build.gradle.kts`: `runIde` (`:141-154`), `prepareSandbox` (`:156-224`), `buildPlugin` (`:107-114`), `patchPluginXml` (`:238-252`), `publishPlugin` (`:232-236`), `pluginVerification` extension (`:130-139`). + +## Custom (defined in this build) + +| Task | Type | Purpose | Inputs/Outputs declared? | +|---|---|---|---| +| `compileDotNet` | `Exec` | `dotnet build --configuration Release` | **No** — re-runs every build | +| `buildResharperPlugin` | `Exec` | `dotnet msbuild $sln /t:Restore;Rebuild;Pack` → produces `.nupkg` | **No** | +| `testDotNet` | `Exec` | `dotnet test $sln --logger GitHubActions` | **No** | + +All three live in `build.gradle.kts:86-105, 226-230`. Captured as refactor candidates in §24 (convert to typed task class with proper `@InputDirectory`/`@OutputDirectory`). + +## Kotlin Gradle plugin + +| Task | Purpose | +|---|---| +| `compileKotlin` | Compile `src/rider/main/kotlin/` to JVM bytecode | +| `compileTestKotlin` | Compile Kotlin tests (none in this project) | +| `kotlinDslAccessorsReport` | DSL accessors diagnostic | + +Configured at `build.gradle.kts:82-84` (sets `JvmTarget.JVM_21`). + +## `:protocol` subproject + +| Task | Type | Purpose | +|---|---|---| +| `:protocol:rdgen` | `RdGenTask` | Run rdgen — generates `*.Generated.kt` and `*.Generated.cs` from `Model.kt` | +| `:protocol:compileKotlin` | (Kotlin plugin) | Compile the protocol DSL itself | + +Configured in `protocol/build.gradle.kts:24-46`. Note rdgen output files are committed; the task is only run manually (`./gradlew :protocol:rdgen`). + +## Java plugin (built-in) + +| Task | Purpose | +|---|---| +| `compileJava` | Compile `.java` sources (this repo has none, but the dir is declared) | +| `processResources` | Copy resources to `build/resources/main/` | +| `classes` | Aggregate of compileKotlin + compileJava + processResources | +| `jar` | Produce `build/libs/-.jar` | +| `wrapper` | Regenerate `gradlew` / `gradle/wrapper/gradle-wrapper.properties` | + +Configured at `build.gradle.kts:14-20` (Java toolchain), `:57-62` (`tasks.wrapper`). + +## How to discover tasks yourself + +```bash +./gradlew tasks # all tasks (grouped by plugin) +./gradlew tasks --all # including hidden ones +./gradlew help --task # detail on one task +./gradlew runIde --dry-run # show task graph without running +./gradlew :protocol:tasks # tasks specific to subproject +``` + +For task implementation classes, look at the IDE inspection on `tasks. {` in your dev IDE — it'll resolve to the contributed plugin's class. + +→ Next: [22 · Glossary](22-glossary.md) diff --git a/docs/wiki/build-system/part-4-reference/22-glossary.md b/docs/wiki/build-system/part-4-reference/22-glossary.md new file mode 100644 index 0000000..c3d6cf0 --- /dev/null +++ b/docs/wiki/build-system/part-4-reference/22-glossary.md @@ -0,0 +1,150 @@ +# 22 · Glossary + +**[Reference]** — *One-line definitions for the obtuse vocabulary used throughout. Skim once; come back when you hit something unfamiliar.* + +## Gradle terms + +**Configuration** +A typed bucket of dependencies and/or artifacts on a project. Used for compile classpaths, runtime classpaths, and custom artifact channels (this repo's `riderModel` is a custom one). + +**Configuration cache** +A Gradle 7.4+ feature that serializes the entire task graph between runs, skipping the configuration phase on subsequent builds. Requires careful coding (no live `Project` access at execution time). Enabled with `--configuration-cache`. + +**Configuration phase** +The middle of Gradle's three-phase lifecycle: where `build.gradle.kts` runs top-to-bottom and tasks get registered/configured. Distinct from execution phase, which runs the task actions themselves. + +**`dependsOn` / `mustRunAfter`** +Task ordering primitives. `dependsOn(t)` says "if I run, t must run first." Does not declare data flow — for that, see "incremental tasks." + +**`Exec` task** +A built-in Gradle task type that runs a command-line process. Used in this repo for `compileDotNet`, `buildResharperPlugin`, `testDotNet`. + +**`extra` / extension extra properties** +A loose `MutableMap` attached to `Project` for ad-hoc properties. Used at `build.gradle.kts:23` for `extra["isWindows"]`. + +**Incremental task** +A task with declared `@InputFiles`/`@OutputFiles` so Gradle can skip it when inputs haven't changed. Tasks without these declarations always re-run. + +**Initialization phase** +The first of Gradle's three lifecycle phases — where `settings.gradle.kts` runs and subprojects are discovered. + +**`pluginManagement`** +A block in `settings.gradle.kts` that controls plugin resolution: where plugins come from, what default versions, custom mappings. + +**Project** +A Gradle entity per buildable directory. The root project + subprojects. Each has its own `build.gradle.kts`. + +**`Property`** +A writable `Provider`. The `something.set(value)` idiom. Used for lazy task wiring — the value can be set or read at any time. + +**`Provider`** +A lazy value. Resolved on first read, often at execution time. The escape hatch for "compute this later" in modern Gradle. + +**`registering` / `register`** +Lazy task registration. `val foo by tasks.registering(Exec::class) { ... }` registers a new task without configuring it eagerly. Modern preference over `tasks.create("foo", Exec::class) { ... }`. + +**Repository** +A source of artifacts (Maven, Ivy, custom). Configured in `repositories { }` blocks. + +**Settings script** +`settings.gradle.kts`. Runs at initialization, configures `pluginManagement`, declares subprojects. + +**Source set** +A logical grouping of sources (e.g. `main`, `test`). This repo's `main` source set is rebound to `src/rider/main/` instead of `src/main/`. + +**Toolchain** +A JVM auto-provisioning mechanism. `java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }` tells Gradle "find or download a JDK 21." + +**Up-to-date check** +Gradle's per-task incrementality decision: skip if all `@InputFiles` are unchanged since the last run. + +**Version catalog** +The `gradle/libs.versions.toml` file. A typed centralization of version strings. Referenced from build scripts via the `libs` accessor (e.g. `alias(libs.plugins.kotlinJvm)`). + +**`withType { }`** +Lazy configuration of all current and future tasks of a given type. Used in `protocol/build.gradle.kts:48` to configure `RdGenTask`s. + +## IntelliJ / Rider plugin terms + +**Bundled module** +A JAR-level unit shipped inside the IDE under `lib/modules/`. Newer than bundled plugins. Some subsystems (like spellchecker in 2024.2+) have migrated from bundled plugin to bundled module status. + +**Bundled plugin** +A plugin that ships pre-installed with the IDE, with its own `plugin.xml` and id. Declared as `bundledPlugin("...")` in IPGP. + +**IPGP / IntelliJ Platform Gradle Plugin v2** +`org.jetbrains.intellij.platform`. The Gradle plugin that does ~70% of this build. Adds tasks, downloads SDKs, manages repos. Documented at . + +**JBR (JetBrains Runtime)** +JetBrains' fork of OpenJDK shipped with IntelliJ-family IDEs. Required for `runIde` because Rider's bundled debugger and Mono integration rely on JBR-specific instrumentation. + +**`patchPluginXml`** +IPGP task that rewrites `plugin.xml` with build-time values (version, change notes, since-build, until-build). + +**`platformPath`** +IPGP API: filesystem path to where the Rider SDK was extracted. Used at `build.gradle.kts:208, :261`. Only valid after IPGP's initialize task has run. + +**Plugin verifier** +JetBrains' compatibility checker. Run via `./gradlew verifyPlugin`. Catches binary-incompat issues against multiple IDE versions. + +**Plugin XML / `plugin.xml`** +The plugin's manifest. Lives at `src/rider/main/resources/META-INF/plugin.xml`. Declares plugin id, dependencies, extension points. + +**Sandbox** +A temporary IDE plugins directory (`build/idea-sandbox/plugins//`) prepared by `prepareSandbox` and loaded by `runIde`. + +**`since-build` / `until-build`** +Plugin compatibility range, declared in `plugin.xml`. Build numbers like `261` = Rider 2026.1. This repo sets `until-build = null` (no upper bound) — see §17. + +## Rider/.NET-specific terms + +**`Directory.Build.props`** +An MSBuild file auto-imported into every `.csproj` in the directory tree. Centralizes .NET version pins, package references, output paths. + +**RD (Reactive Distributed) framework** +JetBrains' typed RPC system used to bridge Rider's JVM frontend and .NET backend. Two libraries: `JetBrains.RdFramework` (.NET runtime), `JetBrains.Rd` (JVM runtime). Source: . + +**rdgen** +The RD code generator. Reads a Kotlin DSL declaring protocol calls/signals/properties and emits matched Kotlin and C# bindings. + +**ReSharper SDK / Rider SDK** +Two NuGet package families. `JetBrains.ReSharper.SDK` is the Wave/ReSharper-for-VS extension API. `JetBrains.Rider.SDK` is its Rider-specific superset. + +**Wave** +ReSharper's internal version stream. Wave 261 = ReSharper 2026.1. Used by the Wave NuGet package as a compatibility constraint for ReSharper-for-VS plugins. + +## Build / .NET terms + +**`dotnet` CLI** +Microsoft's command-line tool for .NET. Wraps MSBuild, NuGet, the test runner. + +**MSBuild** +The .NET build engine. Reads `.csproj`/`.sln` files, runs targets like `Restore`, `Build`, `Pack`. Invoked here through `dotnet msbuild`. + +**NuGet** +.NET's package manager. PackageReferences in `.csproj` files declare dependencies; `dotnet restore` resolves them. + +**`PackageReference`** +A `.csproj` element declaring a NuGet dependency. Modern alternative to `packages.config`. + +**TFM (Target Framework Moniker)** +A short string like `net6.0`, `net8.0` declaring which .NET runtime a project targets. Set via `` in `.csproj`. + +## Project-specific terms + +**`AshAndDust.sln`** +The example mod's solution file. Passed to `runIde` so Rider auto-opens it. + +**Dotfiles / DotFiles plugin** +JetBrains' Unity-debugger helper DLLs. Live under Rider's `plugins/rider-unity/DotFiles/`. Stripped from the cross-platform Maven artifact on Mac/Linux; copied in by the workaround in `build.gradle.kts:188-222`. + +**Frontend / Backend** +This plugin's two halves. Frontend = JVM/Kotlin in Rider's UI process. Backend = .NET/C# in Rider's ReSharper-host process. + +**Remodder** +A plugin feature: decompiles Rimworld's compiled DLLs and shows transpilation results in a tool window. The reason for AsmResolver, ICSharpCode.Decompiler, Lib.Harmony NuGet refs. + +**`riderModel` configuration** +A custom Gradle Configuration in this repo (`build.gradle.kts:254-269`) that exposes `lib/rd/rider-model.jar` from the extracted Rider SDK to the `:protocol` subproject. See §13. + +→ Next: [23 · Where to ask JetBrains](23-where-to-ask-jetbrains.md) diff --git a/docs/wiki/build-system/part-4-reference/23-where-to-ask-jetbrains.md b/docs/wiki/build-system/part-4-reference/23-where-to-ask-jetbrains.md new file mode 100644 index 0000000..309db62 --- /dev/null +++ b/docs/wiki/build-system/part-4-reference/23-where-to-ask-jetbrains.md @@ -0,0 +1,94 @@ +# 23 · Where to ask JetBrains + +**[Reference]** — *When something is upstream's problem, here's where to take it.* + +A Rider plugin author talks to JetBrains regularly. The matrix below maps "thing I'm stuck on" to "where to ask." + +## Slack + +**JetBrains Platform Slack** — sign up at + +Useful channels: +- `#intellij-platform` — general IntelliJ Platform questions, often the right starting point +- `#intellij-platform-rider` — Rider-specific (closer to this plugin's domain) +- `#intellij-platform-resharper` — ReSharper-specific (relevant for the Wave/csproj pattern) +- `#intellij-platform-gradle-plugin` — IPGP-specific bug reports and version-bump questions + +JetBrains engineers are active here. Response times vary, but the signal-to-noise ratio is high. Best for "is this expected?", "did I miss a doc?", "can someone sanity-check this?" + +## YouTrack (issue tracker) + +- **Rider**: +- **ReSharper**: +- **IntelliJ IDEA platform**: +- **rd / RdFramework**: there's no dedicated project; cross-list under RIDER + +For each, search before filing — often the issue is already tracked. + +When filing: +- Title format: short and specific +- Body: include `gradle.properties` snippet, `build.gradle.kts` snippet, exact stack traces, and the IDE/SDK version +- Tag with version and platform if relevant + +## GitHub repositories + +- **`JetBrains/intellij-platform-gradle-plugin`** — + - Issues for IPGP bugs and questions + - Releases for the changelog (consult before bumping) +- **`JetBrains/rd`** — + - rdgen / RdFramework + - Releases for rd-gen versions +- **`JetBrains/resharper-unity`** — + - The closest analog plugin to this one (cross-tier, RD-based) + - When you can't figure out a Rider-plugin pattern, look here for prior art + - Issues are reasonable to file/comment on if your problem touches Unity-style integration +- **`JetBrains/rider-plugin-template`** — + - The official Rider plugin template — mostly what this repo derived from + - Compare its `build.gradle.kts` against this repo's when something is broken; differences usually indicate drift + +## Documentation + +- **IntelliJ Platform plugin SDK home**: +- **Rider plugin development**: +- **ReSharper plugin development (the canonical docs)**: +- **IPGP docs**: +- **Plugin verifier**: +- **Marketplace publishing**: + +The IntelliJ Platform docs are quite good for IntelliJ but Rider-specific guidance is sparse. When the canonical docs don't help, fall back to: +1. Read `JetBrains/resharper-unity` source for an existing pattern +2. Read `JetBrains/rider-plugin-template` for the canonical structure +3. Ask in `#intellij-platform-rider` Slack +4. File a YouTrack issue under RIDER + +## Specific known-issue routing + +For each entry in §17 (quirks and known issues) where the root cause is upstream: + +| Issue | Where to file/ask | +|---|---| +| Mac/Linux DotFiles missing from Rider Maven artifact | YouTrack RIDER (search "DotFiles Maven Rider") | +| IPGP bundled-plugin → bundled-module reclassification surprises on upgrade | IPGP GitHub issues, or `#intellij-platform-gradle-plugin` Slack | +| `useBinaryReleases` flag rename or removal | IPGP GitHub release notes, or `#intellij-platform-gradle-plugin` Slack | +| RdFramework hash mismatches at runtime | rd GitHub issues, or `#intellij-platform-rider` Slack | +| Plugin verifier false-positive (e.g. "TemplateWordInPluginId") | IPGP GitHub issues | + +## What NOT to ask JetBrains + +- "How do I bump my plugin?" — that's in this wiki (§19) and the IPGP changelog +- "Does this Rimworld API exist?" — that's a Rimworld question, not a JetBrains one +- "Why does my code not compile?" — debug it locally first; bring a minimal reproduction if needed + +## What's reasonable to ask + +- "Is the Mac/Linux Maven artifact missing the Unity DotFiles by design?" +- "What's the recommended way to test a Rider plugin end-to-end?" +- "Has the bundled-plugin id for X changed in 2026.2?" +- "Is `provider { }` around `untilBuild` the recommended way to clear it?" +- "What's the right way to declare a custom Configuration that publishes a single SDK file?" (Even though §13 explains it, confirming with JetBrains is worthwhile.) + +## Internal coordination + +If you find that this wiki documents a pattern you needed, but JetBrains' own docs don't, *consider* opening a doc-improvement PR against the IntelliJ Platform docs (). The community has historically been the best source of Rider-plugin lore; sharing back lifts everyone. + +→ Next: [24 · Refactor opportunities](24-refactor-opportunities.md) diff --git a/docs/wiki/build-system/part-4-reference/24-refactor-opportunities.md b/docs/wiki/build-system/part-4-reference/24-refactor-opportunities.md new file mode 100644 index 0000000..2bd66bf --- /dev/null +++ b/docs/wiki/build-system/part-4-reference/24-refactor-opportunities.md @@ -0,0 +1,244 @@ +# 24 · Refactor opportunities + +**[Reference]** — *A backlog of "this is what's NOT ideal and what to do about it." Each item is captured for future maintenance, not committed-to action.* + +The build works today. These are improvements that would reduce surface area, improve incrementality, eliminate drift, or close gaps that bite during upgrades. Each is tied to a specific maintainer goal or a §17 quirk. + +## 1. Consolidate version pinning into `libs.versions.toml` + +**Goal addressed**: "the smaller the surface area I have to worry about, the better" +**Files**: `gradle.properties:24-27` ↔ `gradle/libs.versions.toml`, `build.gradle.kts:9-10`, `settings.gradle.kts:29-34` + +Currently: +- `gradle.properties` holds: `rdVersion`, `rdKotlinVersion`, `intellijPlatformGradlePluginVersion`, `gradleJvmWrapperVersion` +- `libs.versions.toml` holds: `kotlin`, `rdGen` +- `build.gradle.kts` re-declares versions inline for IPGP and jvm-wrapper + +Target state: every version in `libs.versions.toml`. Plugin versions exposed via `[plugins]` table. `pluginManagement` in `settings.gradle.kts` reads via the `versionCatalogs { ... }` block (Gradle 7.4+ permits this). + +Migration: +1. Add to `libs.versions.toml` `[versions]`: `rdGen` (already there), `intellijPlatformGradlePlugin`, `gradleJvmWrapper`, `rdKotlin` +2. Add `[plugins]` entries for all four +3. In `settings.gradle.kts`, replace `String by settings` reads with version-catalog access +4. In `build.gradle.kts`, replace inline `version "..."` with `alias(libs.plugins...)` +5. Delete the corresponding `gradle.properties` lines +6. Test: `./gradlew :buildPlugin` from a clean cache + +Tradeoffs: a couple-day refactor; touches initialization-phase code which can be subtle. Worthwhile because it stops the silent-drift problem. + +## 2. Delete `riderBaseVersion` + +**State**: trivial +**Files**: `gradle.properties:28` + +Zero references in the entire codebase. Delete the line. + +## 3. Delete legacy PowerShell scripts + +**Goal addressed**: "if I can ... rewrite the runVisualStudio.ps1 and buildPlugin.ps1 ... If I'm even using the build one?!" — confirmed: not used by CI +**Files**: `buildPlugin.ps1`, `publishPlugin.ps1`, `settings.ps1`, `tools/vswhere.exe`, `tools/nuget.exe`, `.run/Build ReSharper Plugin.run.xml` + +Steps: +1. Delete the four scripts and `tools/` directory +2. Replace `.run/Build ReSharper Plugin.run.xml` with a Gradle run config calling `:buildResharperPlugin` +3. Update `README.md` if it references the scripts (check first) + +Decision deferred: `runVisualStudio.ps1` has a real local-dev use case (set up an experimental ReSharper hive in Visual Studio for ReSharper-for-VS development). Decide whether to keep it as-is, port it to Kotlin, or delete it depending on whether you actively support that flow. + +## 4. Convert .NET `Exec` tasks to typed task class in `buildSrc/` + +**Goal addressed**: "rewrite the runVisualStudio.ps1 and buildPlugin.ps1 (If I'm even using the build one?!) into a Kotlin class that I can plug into Gradle then that'll make the build system that much more digestible" +**Files**: `build.gradle.kts:86-105, :226-230` + +Create `buildSrc/src/main/kotlin/DotNetBuildTask.kt`: + +```kotlin +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.* +import org.gradle.process.ExecOperations +import javax.inject.Inject + +abstract class DotNetBuildTask @Inject constructor( + private val execOps: ExecOperations +) : DefaultTask() { + @get:InputDirectory abstract val sourceDir: DirectoryProperty + @get:OutputDirectory abstract val outputDir: DirectoryProperty + @get:Input abstract val configuration: Property + @get:Input abstract val solution: Property + + @TaskAction + fun run() { + execOps.exec { + executable = "dotnet" + args("build", solution.get(), "--configuration", configuration.get(), "--consoleLoggerParameters:ErrorsOnly") + } + } +} +``` + +Then in `build.gradle.kts`: + +```kotlin +val compileDotNet by tasks.registering(DotNetBuildTask::class) { + sourceDir.set(layout.projectDirectory.dir("src/dotnet")) + outputDir.set(layout.projectDirectory.dir("src/dotnet/${DotnetPluginId}/bin")) + configuration.set(BuildConfiguration) + solution.set(DotnetSolution) +} +``` + +Result: +- `compileDotNet` is now incremental (skips when no `.cs` changed) +- Configuration cache compatible +- Two more typed tasks for `buildResharperPlugin` and `testDotNet` + +This is the user's stated "rewrite into a Kotlin class that I can plug into Gradle" goal. + +## 5. Extract DotFiles patcher into its own task + +**Files**: `build.gradle.kts:186-222` + +Create `buildSrc/src/main/kotlin/PatchSandboxDotFilesTask.kt` extending `DefaultTask` with `@InputFiles` (candidate Rider install paths) and `@OutputFiles` (destination DLLs). `prepareSandbox` finalizes with it. + +Result: removes the imperative `doLast` from `prepareSandbox`. The side-effect becomes cacheable. + +## 6. Replace the `if (!isWindows)` filesystem walk with a `ValueSource` + +**Files**: `build.gradle.kts:188-201` + +A `ValueSource` is Gradle's mechanism for "compute a value from the environment, configurable-cache-friendly." Probe candidate Rider install paths inside a `ValueSource` rather than imperatively in `doLast`. + +Result: configuration cache works smoothly even with the patcher running. + +## 7. Wrap `CHANGELOG.md` parsing in `provider { }` + +**Files**: `build.gradle.kts:239-247` + +Today the file is read at configuration time (every build, even no-op ones). Move inside `provider { }` for laziness: + +```kotlin +tasks.patchPluginXml { + pluginVersion.set(PluginVersion) + changeNotes.set(provider { + val changelogText = file("${rootDir}/CHANGELOG.md").readText() + .lines() + .dropWhile { !it.trim().startsWith("##") } + .drop(1) + .takeWhile { !it.trim().startsWith("##") } + .filter { it.trim().isNotEmpty() } + .joinToString("\r\n") { + "
  • ${it.trim().replace(Regex("^\\*\\s+?"), "")}
  • " + }.trim() + "
      \r\n$changelogText\r\n
    " + }) + untilBuild.set(provider { null }) +} +``` + +Result: configuration phase faster, no-op builds don't read the file. + +## 8. Drop redundant `apply { plugin("kotlin") }` + +**Files**: `build.gradle.kts:53-55` + +Already covered by `alias(libs.plugins.kotlinJvm)` at line 8. Delete. + +## 9. Replace `${buildDir}` with lazy form + +**Files**: `build.gradle.kts:110` + +```kotlin +val pluginZip = layout.buildDirectory.file("distributions/${rootProject.name}-${version}.zip") +val outputDir = layout.projectDirectory.dir("output").asFile + +tasks.buildPlugin { + doLast { + val zipFile = pluginZip.get().asFile + outputDir.mkdirs() + zipFile.copyTo(outputDir.resolve(zipFile.name), overwrite = true) + } +} +``` + +Mandatory before Gradle 10. Otherwise the build will break loudly. + +## 10. Reconsider `me.filippov.gradle.jvm.wrapper` + +**Files**: `build.gradle.kts:10`, `gradle.properties:27` + +Gradle 7.6+ has built-in JVM toolchain auto-provisioning via the `foojay-resolver-convention` plugin. That covers most of what `gradle-jvm-wrapper` provides for fresh-clone bootstrapping. + +Steps: +1. Add `foojay-resolver-convention` to `pluginManagement.plugins` in `settings.gradle.kts` +2. Remove `id("me.filippov.gradle.jvm.wrapper")` from `build.gradle.kts:10` +3. Remove `gradleJvmWrapperVersion` from `gradle.properties:27` and `settings.gradle.kts:7` +4. Test on a machine without JDK 21 installed: `./gradlew runIde` + +Tradeoff: removes one duplicated version pin and one third-party plugin dep. + +## 11. Update `.run/Build Plugin.run.xml` JDK reference + +**Files**: `.run/Build Plugin.run.xml:8` + +Change `corretto-17.0.7` to `corretto-21`, or remove the `JAVA_HOME` env var entirely (the toolchain handles it). + +## 12. Add a fixture-mod-driven `:integrationTest` task + +**Goal addressed**: "Including a mod as a fixture should help make it easier to launch and test with new versions" +**Approach**: + +1. Promote `example-mod/` to a real fixture mod (or create `tests/fixture-mod/` with a more controlled setup) +2. Add a Gradle `:integrationTest` task that: + - Depends on `prepareSandbox` + - Boots Rider headlessly against the fixture + - Asserts known completion items / references / run-config behavior +3. Register the task in CI's `Test` job + +This is a substantial undertaking. JetBrains' `RiderTestBase` infrastructure is undocumented; contact JetBrains via Slack (§23) before starting. + +## 13. Fill in `ReSharperPlugin.RimworldDev.Tests` + +**Goal addressed**: testability ramp +**Files**: `src/dotnet/ReSharperPlugin.RimworldDev.Tests/` + +Steps: +1. Add `` to the test csproj +2. Add a first ReSharper-style fixture for `RimworldXMLItemProvider` (using `BaseTestWithSingleProject` or similar) +3. Use `JetBrains/resharper-unity`'s `Unity.Tests.csproj` as the reference + +Lower-effort than #12 — pure ReSharper tests don't need to boot a Rider host. + +## 14. Add a `verifyDotNetOutputs` task + +**Files**: `build.gradle.kts:181-184` + +Decouple the existence-check `doLast` from `prepareSandbox`. Create a separate task with proper `@InputFiles`. `prepareSandbox` becomes pure declarative file copying; `verifyDotNetOutputs` runs as part of the chain. Cleaner separation of concerns and gives `prepareSandbox` better incrementality. + +## 15. Wire up `verifyPlugin` in CI + +**Files**: `.github/workflows/CI.yml` + +The `verifyPlugin` task isn't currently run by CI. Add a step: + +```yaml +- run: ./gradlew :verifyPlugin --no-daemon +``` + +Result: catch binary-incompat issues across Rider versions before they ship. + +## Prioritization (a suggestion, not a mandate) + +| Priority | Item | Rationale | +|---|---|---| +| High | #2, #8, #11 | Trivial cleanups, immediate signal-to-noise improvement | +| High | #1 | The user's stated "consolidate versions" goal; affects all future bumps | +| High | #9 | Mandatory before Gradle 10 | +| Medium | #3 | The user's "PowerShell" question; cleaner repo | +| Medium | #4 | Improves incrementality and config cache; user's stated "Kotlin class" goal | +| Medium | #15 | Catches platform-incompat issues earlier | +| Lower | #5, #6, #7 | Polish; unblocks config cache improvements | +| Lower | #10 | Removes a dependency; not urgent | +| Big effort | #12, #13 | The user's testability goal; needs JetBrains help (§23) | +| Polish | #14 | Cleaner code; not strictly necessary | + +→ End of wiki.