# Perl integration

<Badge text="⭐ Community Toolkit" variant="tip" size="large" />

<Image
  src={perlIcon}
  alt="Perl Camel"
  width={75}
  height={75}
  class:list={'float-inline-left icon'}
  data-zoom-off
/>

This document explains how key hosting API calls map to on-disk
directory layout, environment variable configuration, and runtime behavior.

## Installation

To start building an Aspire app that uses Meilisearch, install the [📦 CommunityToolkit.Aspire.Hosting.Perl](https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Perl) NuGet package:

```bash title="Terminal"
aspire add CommunityToolkit.Aspire.Hosting.Perl
```

<LearnMore>
  Learn more about [`aspire add`](/reference/cli/commands/aspire-add/) in the command reference.
</LearnMore>

Or, choose a manual installation approach:

```csharp title="C# — AppHost.cs"
#:package CommunityToolkit.Aspire.Hosting.Meilisearch@*
```

```xml title="XML — AppHost.csproj"
<PackageReference Include="CommunityToolkit.Aspire.Hosting.Perl" Version="*" />
```

```bash title="Terminal"
aspire add CommunityToolkit.Aspire.Hosting.Perl
```

<LearnMore>
  Learn more about [`aspire add`](/reference/cli/commands/aspire-add/) in the command reference.
</LearnMore>

This updates your `aspire.config.json` with the Meilisearch hosting integration package:

```json title="aspire.config.json" ins={3}
{
  "packages": {
    "CommunityToolkit.Aspire.Hosting.Perl": "*"
  }
}
```

## Add Perl resource

Add a Perl script resource in your `AppHost.cs`:

```csharp
var builder = DistributedApplication.CreateBuilder(args);

builder.AddPerlScript("my-worker", "scripts", "Worker.pl")
    .WithCpanMinus()
    .WithPackage("Some::Module", skipTest: true)
    .WithLocalLib("local");

builder.Build().Run();
```

## Core concepts

The integration provides two entry points for adding Perl resources:

| Method | Purpose |
|--------|---------|
| `AddPerlScript(name, appDirectory, scriptName)` | Adds a Perl script (worker, CLI tool, etc.) |
| `AddPerlApi(name, appDirectory, scriptName)` | Adds a Perl API server (e.g., Mojolicious `daemon`) |

Both create a `PerlAppResource` that appears in the Aspire dashboard. All subsequent configuration
methods (`.WithCpanMinus()`, `.WithLocalLib()`, etc.) chain off the resource builder.

## The `appDirectory` Parameter

`appDirectory` is the **anchor for all relative path resolution** in the integration. It determines:

- The resource's `WorkingDirectory` — where Perl runs
- Where `WithLocalLib("local")` resolves to
- Where cpanfile discovery happens (for `WithProjectDependencies`)
- The base for the script path

`appDirectory` is resolved relative to the **AppHost project directory** (the folder containing
the `.csproj`).

### `"."` — AppHost-rooted

When `appDirectory` is `"."`, the working directory is the AppHost project folder itself. Files like
`cpanfile`, `cpanfile.snapshot`, and the `local/` directory all live alongside the `.csproj`:

- MyApp.AppHost
  - AppHost.cs
  - MyApp.AppHost.csproj
  - cpanfile  discovered here
  - cpanfile.snapshot
  - local  WithLocalLib("local") resolves here
    - lib/perl5
  - Properties
- scripts
  - API.pl  script path "../scripts/API.pl"

### `"../scripts"` — sibling folder

When `appDirectory` is `"../scripts"`, the working directory shifts to a sibling `scripts/` folder.
Everything resolves relative to that folder:

- MyApp.AppHost
  - AppHost.cs
  - MyApp.AppHost.csproj
  - Properties/
- scripts  working directory
  - Worker.pl  script path "Worker.pl"
  - local  WithLocalLib("local") resolves here
    - lib/perl5

:::note
**Key insight:** The script path in `AddPerlScript` / `AddPerlApi` is relative to `appDirectory`, and so is everything else — `WithLocalLib`, cpanfile discovery, and the process working directory.
:::

## WithLocalLib

```csharp
.WithLocalLib("local")         // relative path — resolved against appDirectory
.WithLocalLib("/opt/lib")      // rooted Unix-style path — used as-is
.WithLocalLib("C:\\perl-lib")  // rooted Windows path — used as-is
```

`WithLocalLib` configures [local::lib](https://metacpan.org/pod/local::lib)-style module isolation.
The `path` parameter is resolved **relative to the resource's working directory** (`appDirectory`),
not relative to the AppHost project, unless the path is already rooted.

Implementation note: `WithLocalLib` path resolution uses `Path.IsPathRooted(configuredPath)`.
If `true`, the value is used directly. If `false`, it is combined with the resource working
directory and converted to an absolute path.

### What it sets

| Environment Variable | Value |
|---------------------|-------|
| `PERL5LIB` | `<resolved>/lib/perl5` |
| `PERL_LOCAL_LIB_ROOT` | `<resolved>` |
| `PERL_MM_OPT` | `INSTALL_BASE=<resolved>` |
| `PERL_MB_OPT` | `--install_base <resolved>` |

These ensure that:
- Perl finds modules in the local directory at runtime (`@INC`)
- Package managers install modules into the local directory
- No `sudo` or system-level permissions required

### Resolution examples

| `appDirectory` | `WithLocalLib(...)` | Resolved absolute path |
|----------------|---------------------|----------------------|
| `"."` | `"local"` | `<AppHost>/local` |
| `"../scripts"` | `"local"` | `<AppHost>/../scripts/local` |
| `"."` | `"/opt/perl-libs"` | `/opt/perl-libs` (Linux/macOS) |
| `"."` | `"C:\\perl-libs"` | `C:\\perl-libs` (Windows) |

## Package management

While I highly recommend you use cpanm or Carton, the integration aims to support three package managers and two installation strategies:

| Package Manager | Individual Packages | Project Dependencies |
|----------------|-------------------|---------------------|
| **cpan** (default) | ✅ `.WithPackage("Module")` | ❌ Not supported (auto-switches to cpanm when calling `.WithProjectDependencies()`) |
| **cpanm** (App::cpanminus) | ✅ `.WithCpanMinus().WithPackage("Module")` | ✅ `.WithCpanMinus().WithProjectDependencies()` |
| **Carton** | ❌ Not supported | ✅ `.WithCarton().WithProjectDependencies()` |

:::tip
The default package manager is `cpan`, but it is automatically switched to `cpanm` when
`WithProjectDependencies()` is called, since `cpan` does not support `--installdeps`.
`WithLocalLib()` will also currently swap to `cpanm` because it wasn't clear to me at time of release how to integrate it with cpan.
:::

### WithCpanMinus + WithPackage

Installs individual modules by name before the application starts.

```csharp
builder.AddPerlScript("worker", "../scripts", "Worker.pl")
    .WithCpanMinus()
    .WithPackage("OpenTelemetry::SDK", skipTest: true)
    .WithLocalLib("local");
```

**What happens at startup:**

1. A child installer resource runs `cpanm --notest --local-lib <resolved>/local OpenTelemetry::SDK`
2. The module is installed into `scripts/local/lib/perl5/`
3. After installation, the main script starts with `PERL5LIB` pointing to the local directory

**Resulting directory structure:**

- my-example
  - MyExample.AppHost/
    - AppHost.cs
    - MyExample.AppHost.csproj
  - scripts  working directory (appDirectory = "../scripts")
    - Worker.pl
      - local
        - lib
          - perl5
            - OpenTelemetry/
              - SDK.pm

**Options:**

| Parameter | Effect |
|-----------|--------|
| `force: true` | Passes `--force` — reinstalls even if already present |
| `skipTest: true` | Passes `--notest` — skips running the module's test suite |

### WithCpanMinus + WithProjectDependencies

Installs all modules listed in a `cpanfile` in the working directory.

```csharp
builder.AddPerlApi("api", ".", "../scripts/API.pl")
    .WithCpanMinus()
    .WithProjectDependencies()
    .WithLocalLib("local");
```

**What happens at startup:**

1. The integration looks for `cpanfile` in the working directory
2. Runs `cpanm --installdeps --notest .` (with `--local-lib` if configured)
3. All dependencies from the cpanfile are installed

**Expected cpanfile location:** `<appDirectory>/cpanfile`

### WithCarton + WithProjectDependencies

[Carton](https://metacpan.org/pod/Carton) is a dependency manager for Perl that provides
reproducible builds via a lock file (`cpanfile.snapshot`).

```csharp
builder.AddPerlApi("api", ".", "../scripts/API.pl")
    .WithCarton()
    .WithProjectDependencies(cartonDeployment: false)
    .WithLocalLib("local");
```

**What happens at startup:**

1. The integration looks for `cpanfile` and optionally `cpanfile.snapshot` in the working directory
2. Runs `carton install` (or `carton install --deployment` if `cartonDeployment: true`)
3. Carton creates `local/` adjacent to the `cpanfile`

**Deployment mode (`cartonDeployment: true`):** Installs exact versions from `cpanfile.snapshot`,
ensuring production builds match development. Fails if the snapshot is missing or out of date.

**Resulting directory structure (appDirectory = "."):**

- my-example
  - MyApp.AppHost  working directory (appDirectory = ".")
    - AppHost.cs
    - MyApp.AppHost.csproj
    - cpanfile
    - cpanfile.snapshot
    - local
      - lib
        - perl5
          - Mojolicious
  - scripts
    - API.pl  script path "../scripts/API.pl"

:::important
Carton only supports project-level dependency installation. Calling `.WithPackage()` after `.WithCarton()` will throw an `InvalidOperationException`. If you need to install individual modules alongside Carton-managed dependencies, use `.WithCpanMinus()` on a separate resource.
:::

## WithPerlbrewEnvironment

[Perlbrew](https://perlbrew.pl/) manages multiple Perl installations. This method configures the
resource to use a specific perlbrew-managed Perl version.

```csharp
builder.AddPerlScript("perlbrew-worker", "../scripts", "Worker.pl")
    .WithPerlbrewEnvironment("perl-5.42.0");
```

**What it configures:**
- Resolves the Perl binary from the perlbrew installation
- Sets `PERLBREW_ROOT`, `PERLBREW_PERL`, and `PERLBREW_HOME`
- Prepends the perlbrew `bin/` to `PATH`

**Interaction with WithLocalLib:** If `.WithLocalLib("local")` is chained, modules are installed
into the local directory, not the perlbrew tree. This keeps the perlbrew installation clean and
allows per-project isolation. `WithLocalLib` is optional when using perlbrew.

:::note
Perlbrew is Linux-only. On Windows, the integration will display a notification recommending [Berrybrew](https://github.com/stevieb9/berrybrew). Windows support for Berrybrew is on the roadmap.
:::

## Common Pitfalls

### WithLocalLib resolves relative to `appDirectory`, not the AppHost

```csharp
// appDirectory = "../scripts", WithLocalLib("local")
// ✅ Resolves to: scripts/local/
// ❌ Does NOT resolve to: MyApp.AppHost/local/
```

If you expect the `local/` folder next to your `.csproj`, set `appDirectory` to `"."`.

### Choosing to skip WithLocalLib modifies shared Perl installs

It is valid to skip `WithLocalLib` if you intentionally want a shared/global module install.
That can be useful for common libraries on dev machines.

The tradeoff is that installs target your platform Perl distribution instead of a project-local
folder. In practice this often means:

- Linux (especially OS-managed Perl): writes to system or user Perl paths and may require elevated permissions
- Windows: writes into the active Strawberry Perl or ActiveState Perl environment

This can be convenient, but it can also create drift across machines and affect unrelated projects.
Proceed with caution.

### Mixing WithCarton and WithPackage

Carton manages all dependencies through `cpanfile`. Calling `.WithPackage()` after `.WithCarton()`
will throw an `InvalidOperationException`:

```csharp
// ❌ This throws — Carton does not support individual module installation
builder.AddPerlApi("api", ".", "api.pl")
    .WithCarton()
    .WithPackage("Some::Module");

// ✅ Instead, add the module to your cpanfile:
//    requires 'Some::Module';
```

### Script path is relative to `appDirectory`

The `scriptName` parameter is resolved relative to `appDirectory`. Don't include the `appDirectory`
in the script path:

```csharp
// ❌ Double-nests the path
builder.AddPerlScript("worker", "../scripts", "../scripts/Worker.pl");

// ✅ Correct — script path is relative to appDirectory
builder.AddPerlScript("worker", "../scripts", "Worker.pl");
```

### cpanfile must be in the working directory

`WithProjectDependencies` looks for `cpanfile` in the resource's working directory (`appDirectory`).
If your cpanfile is in a different location, adjust `appDirectory` accordingly.

### cpanfile example

Use a `cpanfile` to declare project dependencies for `WithProjectDependencies()`.

```perl
requires 'Mojolicious', '>= 9.0';
requires 'OpenTelemetry::SDK';

on 'test' => sub {
    requires 'Test::More', '>= 1.302190';
};
```

Further reading:
- [CPAN::cpanfile reference](https://github.com/miyagawa/cpanfile/blob/master/README.md)