diff --git a/src/frontend/config/sidebar/integrations.topics.ts b/src/frontend/config/sidebar/integrations.topics.ts index d4f2ea622..adda040d1 100644 --- a/src/frontend/config/sidebar/integrations.topics.ts +++ b/src/frontend/config/sidebar/integrations.topics.ts @@ -1336,6 +1336,7 @@ export const integrationTopics: StarlightSidebarTopicsUserConfig = { { label: 'JavaScript', slug: 'integrations/frameworks/javascript' }, { label: 'Node.js extensions', slug: 'integrations/frameworks/nodejs-extensions' }, { label: 'Orleans', slug: 'integrations/frameworks/orleans' }, + { label: 'Perl', slug: 'integrations/frameworks/perl' }, { label: 'PowerShell', slug: 'integrations/frameworks/powershell' }, { label: 'Python', slug: 'integrations/frameworks/python' }, { label: 'Rust', slug: 'integrations/frameworks/rust' }, diff --git a/src/frontend/src/assets/icons/perl-096-1500.svg b/src/frontend/src/assets/icons/perl-096-1500.svg new file mode 100644 index 000000000..35b3f2eba --- /dev/null +++ b/src/frontend/src/assets/icons/perl-096-1500.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/frontend/src/content/docs/community/contributor-guide.mdx b/src/frontend/src/content/docs/community/contributor-guide.mdx index 29b866d03..aca43ae5b 100644 --- a/src/frontend/src/content/docs/community/contributor-guide.mdx +++ b/src/frontend/src/content/docs/community/contributor-guide.mdx @@ -174,6 +174,48 @@ Without the blank lines between steps, the inner content won't render correctly. +## 🧩 Adding a new framework integration + +If you've built a new Community Toolkit hosting integration (e.g. `CommunityToolkit.Aspire.Hosting.`) and want to document it on `aspire.dev`, you'll need to touch several files. Here's a summary of the steps, using the Perl integration as an example: + + + +1. **Create the documentation page** + + Add a new MDX file at `src/frontend/src/content/docs/integrations/frameworks/.mdx`. Use an existing framework page (such as `python.mdx` or `java.mdx`) as a template. Include frontmatter with a `title`, any required component imports, and a `Badge` indicating it's a Community Toolkit integration. + +1. **Add an icon asset** + + Place an SVG icon for the framework in `src/frontend/src/assets/icons/`. Reference it in your MDX page with an `Image` component import. + +1. **Register the sidebar entry** + + In `src/frontend/config/sidebar/integrations.topics.ts`, add a new entry for your framework in the frameworks list (keep alphabetical order): + + ```ts + { label: '', slug: 'integrations/frameworks/' }, + ``` + +1. **Add the NuGet package name** + + In `src/frontend/src/data/aspire-integration-names.json`, add the full NuGet package name for your integration (keep alphabetical order): + + ```json + "CommunityToolkit.Aspire.Hosting.", + ``` + +1. **Add license attribution** + + If your integration uses assets or technologies with specific licenses, add entries in `src/frontend/src/data/thanks-license-titles.ts`: + + ```ts + '': ': ', + ``` + + + +After making these changes, run `pnpm dev` (or `aspire start`) locally to verify the page renders correctly, the sidebar navigation works, and the icon displays as expected. + ## ✍️ Writing style guide diff --git a/src/frontend/src/content/docs/integrations/frameworks/perl.mdx b/src/frontend/src/content/docs/integrations/frameworks/perl.mdx new file mode 100644 index 000000000..aec50a572 --- /dev/null +++ b/src/frontend/src/content/docs/integrations/frameworks/perl.mdx @@ -0,0 +1,390 @@ +--- +title: Perl integration +--- + +import { Badge } from '@astrojs/starlight/components'; +import { Image } from 'astro:assets'; +import perlIcon from '@assets/icons/perl-096-1500.svg'; + + + +Perl Camel + +## Using the Perl Hosting Integration + +A guide for developers using `CommunityToolkit.Aspire.Hosting.Perl` for the first time, or as a +reference when revisiting the API. This document explains how key hosting API calls map to on-disk +directory layout, environment variable configuration, and runtime behavior. + +--- + +## Table of Contents + +1. [Quick Start](#quick-start) +2. [Core Concepts](#core-concepts) +3. [The `appDirectory` Parameter](#the-appdirectory-parameter) +4. [WithLocalLib](#withlocallib) +5. [Package Management](#package-management) + - [WithCpanMinus + WithPackage](#withcpanminus--withpackage) + - [WithCpanMinus + WithProjectDependencies](#withcpanminus--withprojectdependencies) + - [WithCarton + WithProjectDependencies](#withcarton--withprojectdependencies) +6. [WithPerlbrewEnvironment](#withperlbrewenvironment) +7. [Common Pitfalls](#common-pitfalls) +8. [Reporting an Issue](#reporting-issues) + +--- + +## Quick Start + +Install the package in your AppHost project: + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.Perl +``` + +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/... +``` + +> **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` | `/lib/perl5` | +| `PERL_LOCAL_LIB_ROOT` | `` | +| `PERL_MM_OPT` | `INSTALL_BASE=` | +| `PERL_MB_OPT` | `--install_base ` | + +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"` | `/local` | +| `"../scripts"` | `"local"` | `/../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()` | + +> 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 /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:** `/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) + +## Reporting Issues + +If you notice an issue with these docs while using the integration, please file an issue at [microsoft/aspire.dev](https://github.com/microsoft/aspire.dev) \ No newline at end of file diff --git a/src/frontend/src/data/aspire-integration-names.json b/src/frontend/src/data/aspire-integration-names.json index 098d801bf..23b70f67e 100644 --- a/src/frontend/src/data/aspire-integration-names.json +++ b/src/frontend/src/data/aspire-integration-names.json @@ -117,6 +117,7 @@ "CommunityToolkit.Aspire.Hosting.Ollama", "CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector", "CommunityToolkit.Aspire.Hosting.PapercutSmtp", + "CommunityToolkit.Aspire.Hosting.Perl", "CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions", "CommunityToolkit.Aspire.Hosting.PowerShell", "CommunityToolkit.Aspire.Hosting.Python.Extensions", diff --git a/src/frontend/src/data/thanks-license-titles.ts b/src/frontend/src/data/thanks-license-titles.ts index 57f292c8a..372618bc8 100644 --- a/src/frontend/src/data/thanks-license-titles.ts +++ b/src/frontend/src/data/thanks-license-titles.ts @@ -49,4 +49,6 @@ export const licenseTitles = { dapr: 'Apache-2.0: https://github.com/dapr/dapr/blob/master/LICENSE', hex1b: 'MIT: https://hex1b.dev', asciinema: 'GPL-3.0: https://github.com/asciinema/asciinema/blob/develop/LICENSE', + perlAssets: 'CC-BY-4.0: https://github.com/metacpan/perl-assets?tab=CC-BY-4.0-1-ov-file#readme', + perl: 'Artistic License: https://dev.perl.org/licenses/artistic.html', } as const; \ No newline at end of file