Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/content/en-US/docs/3.extending/1.plugins.md
blob: 1592632b3908389113346abe320a378bf507787c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
---
title: "Writing a plugin"
description: "A plugin is a JAR that extends the project DSL."
source: "https://github.com/Progmasoft/tazel/blob/main/Documents/PLUGINS.md"
---

<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> -->
<!-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 -->

A plugin is a JAR that extends the project DSL. It can add blocks a project file uses, such as `cmake { }`, add steps to
the build of the project, record values in its plan and check the project once it is complete. This page shows how a
plugin is built, how Tazel finds and verifies it, and what it may do once it runs.

A plugin is trusted build logic. It runs inside Tazel with the full access of the JVM; Tazel checks that the JAR is the
one the project asked for, and does not sandbox what it does.

## Using a plugin

A project declares its plugins in a `plugins` block:

```kotlin
plugins {
  plugin("Publisher") {
    name = "CMake"
    version = "0.1.0"
  }

  localPlugin {
    path = "Tools/plugin.jar"
  }
}
```

- `plugin("Publisher") { name = ...; version = ... }` asks for a plugin of ViGet. Tazel looks in the plugin cache of the
  project, `.tazel/plugins`, first; when no matching version is there, it downloads the plugin from
  `https://viget.progmasoft.com/dslplugins/tazel/<Publisher>/<Name>/` into the cache. Without `version`, the newest
  version that is not yanked and was built for the plugin API of this Tazel is taken; a version that is named is taken
  even when it was yanked.
- `localPlugin { path = ... }` names a JAR inside the project directory. Its publisher, name and version come from the
  JAR itself.

Tazel reads this block **before** it runs the rest of the file, with a small parser that runs no code. That parser
accepts only what the example shows: `plugin("...")` and `localPlugin`, with fields written as plain string literals; a
value computed from a variable or a string template is refused. Comments and strings elsewhere in the file are skipped,
so a `plugins` word inside them is not mistaken for the block.

When the script runs, the block declares the same plugins again, and every plugin the parser found must be declared.
Write the block at the top level of the file: inside an `if` that does not run, its plugins stay undeclared and the
command fails with `resolved plugins were not declared by the script`.

## Building a plugin

A plugin is built with Gradle and the Gradle plugin `com.progmasoft.tazel.plugin`, whose version is the version of Tazel
the plugin is built for:

```kotlin
// build.gradle.kts of the plugin
plugins {
  kotlin("jvm") version "2.4.0"
  id("com.progmasoft.tazel.plugin") version "0.4.0"
}

version = "0.1.0"

kotlin { jvmToolchain(25) }

tazelPlugin {
  publisher = "Publisher"
  name = "CMake"
  pluginClass = "com.example.cmake.CMakePlugin"
  imports = listOf("com.example.cmake.dsl.*")
  description = "Configures and builds CMake projects."
  authors = listOf("Ada Lovelace <ada@example.com>")
  license = "MIT"
  homepage = "https://example.com/cmake"
  repository = "https://github.com/example/cmake-plugin"
}

dependencies {
  tazelApi()
}
```

The Gradle plugin

- adds the repository of the API, `https://maven.progmasoft.com/tazel`, for the group `com.progmasoft.tazel` only;
- makes `tazelApi()` mean `compileOnly` and `testImplementation` of `com.progmasoft.tazel:tazel-api` in its own version.
  The API is not bundled: Tazel provides it, and the Kotlin standard library, when it loads the plugin;
- writes the descriptor and the service entry below from `tazelPlugin { }` into the JAR. The version of the plugin is
  the version of the project unless `tazelPlugin.version` sets another.

`gradlew jar` then writes the plugin JAR.

## The JAR

A plugin JAR holds:

```text
com/example/cmake/CMakePlugin.class
com/example/cmake/dsl/CMake.class                                  the functions it adds to the DSL
META-INF/tazel-plugin.properties                                   the descriptor
META-INF/services/com.progmasoft.api.tazel.dsl.plugin.ProjectPlugin
```

### The descriptor

```properties
publisher=Publisher
name=CMake
version=0.1.0
apiVersion=2
imports=com.example.cmake.dsl.*
description=Configures and builds CMake projects.
author.0=Ada Lovelace <ada@example.com>
license=MIT
homepage=https\://example.com/cmake
repository=https\://github.com/example/cmake-plugin
```

| Key | Rule |
| --- | --- |
| `publisher` | one identifier: the name of a Progmasoft Account |
| `name` | one identifier |
| `version` | an exact version: `A.B.C`, then `.D`, `.E` and `-METADATA` if needed |
| `apiVersion` | the plugin API it was built for; Tazel loads only `2`, the current `PROJECT_PLUGIN_API_VERSION` |
| `imports` | optional; comma-separated packages or classes the project file imports by default, such as `com.example.cmake.dsl.*` |
| `description`, `author.0`, `author.1`, ..., `license`, `homepage`, `repository` | optional for Tazel; ViGet shows them on the page of the plugin. `license` is an SPDX expression. |

An import must be a dotted name, optionally ending in `.*`; anything else is refused.

### The service

`META-INF/services/com.progmasoft.api.tazel.dsl.plugin.ProjectPlugin` names the class that implements `ProjectPlugin`:

```text
com.example.cmake.CMakePlugin
```

The entry names exactly one class; a line may end in a `#` comment. The class implements `ProjectPlugin` and has a
public constructor without parameters. Who the plugin is comes from the descriptor alone: the class does not state its
publisher, name or version.

## The API

The API is `com.progmasoft.tazel:tazel-api`, the packages `com.progmasoft.api.tazel` and below. A plugin implements
`ProjectPlugin`:

```kotlin
fun interface ProjectPlugin {
  fun apply(context: ProjectPluginContext)
}

interface ProjectPluginContext {
  val projectRoot: Path
  val host: Host

  fun registerExtension(name: String)
  fun contribute(key: String, value: String)
  fun onFinalize(action: (ProjectSummary) -> Unit)
}
```

| Call | What it does |
| --- | --- |
| `projectRoot` | the directory of the project file |
| `host` | the operating system and the architecture Tazel runs on |
| `registerExtension(name)` | claims a DSL name, such as `cmake`, for this plugin. A name is one identifier, and two plugins cannot claim the same one. |
| `contribute(key, value)` | records a value in the plan of the project. A key starts with a letter or `_` and holds at most 128 letters, digits, `_`, `.` or `-`; each key is written once per plugin, and the value cannot be empty. |
| `onFinalize(action)` | runs `action` with a `ProjectSummary` (name, version, build mode, output directory) after the script ended, for checks that need the whole project |

The extensions and contributions of each plugin end up in the plan of the project and in its lockfile.

### Blocks and build steps

The block a project file writes, such as `cmake { }`, is a function the plugin defines in one of its `imports`. It asks
`PluginExtensions` for the scope of its extension, which fails unless the project declared the plugin that registered
it, and adds the work of the block as build steps:

```kotlin
package com.example.cmake.dsl

import com.progmasoft.api.tazel.dsl.TazelPluginDsl
import com.progmasoft.api.tazel.dsl.plugin.PluginExtensions

@TazelPluginDsl
class CMakeScope {
  var enabled = false
  internal val definitions = linkedMapOf<String, String>()

  fun define(vararg values: Pair<String, String>) = definitions.putAll(values)
}

fun cmake(block: CMakeScope.() -> Unit) {
  val settings = CMakeScope().apply(block)
  if (!settings.enabled) return
  val scope = PluginExtensions.require("cmake")
  scope.buildStep("configure") { context ->
    val cmake = context.findProgram("cmake") ?: context.fail("cmake was not found on PATH")
    val exitCode =
      context.run(
        listOf(cmake.toString(), "-S", ".", "-B", context.workDirectory.toString()) +
          settings.definitions.map { (key, value) -> "-D$key=$value" }
      )
    if (exitCode != 0) context.fail("cmake exited with $exitCode")
  }
}
```

`tazel build` and `tazel run` run the steps of a project in the order they were added, before its Tala targets compile.
For `tazel build`, a project whose plugins add steps needs no Tala target; `tazel run` needs an executable to run and
runs the steps of its project before it compiles it. A step runs with a `BuildContext`:

| Member | What it is |
| --- | --- |
| `projectRoot`, `host`, `buildMode` | the project, the machine and `DEBUG` or `RELEASE` |
| `project` | the `ProjectSummary` of the project: its name, version, build mode and output directory |
| `outputDirectory` | the absolute directory the build mode of the project writes artifacts to |
| `workDirectory` | a directory of the step for intermediate files: `.tazel/build/<Publisher>.<Name>/<step>` of the project, the same in every build |
| `status(verb, message)`, `warn(message)` | prints a line in the output of Tazel |
| `run(command, directory, environment)` | runs a program, with variables added to those of Tazel, and returns its exit code; what it prints goes to the output of Tazel |
| `capture(command, directory, environment)` | runs a program like `run`, but returns its exit code and what it printed instead of showing it: for a probe whose failure is an answer, or a program whose output the step reads |
| `findProgram(name)` | the program on `PATH`, with the executable extension of the host, or `null` |
| `fail(message)` | stops the build with the coordinate of the plugin, the name of the step and the message |

`@TazelPluginDsl` marks the scopes of a plugin, so that a nested block cannot reach the members of the block around it by
mistake.

A failure inside `apply`, a finalizer or a step stops the command with the coordinate of the plugin and the message of
the exception, for example `plugin 'Publisher.CMake' failed while finalizing the project: the CMake plugin needs a
CMakeLists.txt`.

## How Tazel loads a plugin

1. **Resolve.** A local plugin must be a regular file, not a symbolic link, inside the project directory, also after its
   real path is resolved. A ViGet plugin is looked up in `.tazel/plugins` by publisher, name and, when given, version;
   when it is not there, its index, `index.json`, and its JAR, `<Version>/<Name>-<Version>.jar`, are fetched from ViGet,
   and the JAR is kept only when its SHA-256 is the one the index lists.
2. **Verify.** Tazel reads the descriptor, checks every key and computes the SHA-256 digest of the JAR. When a file
   `<plugin>.jar.sha256` lies beside the JAR, its first word must be that digest, in lower-case hex.
3. **Activate.** The JARs are put on a class loader of their own; for each plugin, the class its service entry names is
   created with its public constructor, and `apply` runs in the order of the declarations.
4. **Configure.** The project file runs. Its `plugins` block declares the same plugins again; each must have been resolved
   in step 1 with the version the block names. The blocks of the plugins add their build steps.
5. **Finalize.** After the script, every finalizer runs with the summary of the project, and the plugin entries are added
   to its plan.
6. **Lock.** The publisher, name, version, API version and digest of every plugin are recorded in `LOCK.sqlite3`; see
   [The lockfile](/docs/reference/lockfile).
7. **Build.** `tazel build`, and `tazel run` for the project of the executable it runs, run the build steps. The JARs stay open until the command ends.

## Trying a plugin

The quickest loop is a project of its own that names the JAR as a local plugin:

```kotlin
plugins {
  localPlugin { path = "build/libs/cmake-plugin-0.1.0.jar" }
}

project { name = "Demo" }

cmake {
  enabled = true
  define("BUILD_TESTS" to "OFF")
}
```

`tazel build` runs the steps of the plugin. `tazel lock resolve` loads the plugin, runs the script and writes the
lockfile without building anything; `tazel lock create-dump` then shows the plugin as the lockfile records it.