Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/wiki/build-system/00-start-here.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
136 changes: 136 additions & 0 deletions docs/wiki/build-system/part-1-foundation/03-gradle-101-just-enough.md
Original file line number Diff line number Diff line change
@@ -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<T>("name") { ... }`
tasks.runIde {
dependsOn(compileDotNet)
}

// Configure ALL tasks of a given type (now and in the future)
tasks.withType<RdGenTask> {
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<T>` 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)
Loading
Loading