Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Unfree vendor binaries are the exception: they live in `pkgs/default.nix`'s `unf

**`pkgs/images/`** — container images built with `nix2container.buildImage`. Images are not standalone packages; they attach to an existing nixpkgs package via `overrideAttrs` + `passthru.image` (see `hercules-ci-agent` and `github-runner` in `pkgs/default.nix`).

**`lib/salesforce/`** — `mkSfPlugin` builds an oclif plugin from its registry tarball into `$out/lib/node_modules/<npmName>`; `sfWithPlugins` backs `salesforce-cli.withPlugins`, which relinks the CLI with those plugins as core plugins. oclif loads a core plugin only when it appears in both `oclif.plugins` and `dependencies` of the CLI's `package.json`, and node resolves a module's dependencies from its realpath, so the CLI's own files are copied while its `node_modules` stay symlinks. A plugin that shells out to another program declares it in `runtimeInputs`; `withPlugins` puts those on the CLI wrapper's PATH.

**`lib/maintainers.nix`** — extends `pkgs.lib.maintainers` with the local `UnstoppableMango` entry. Referenced in every `meta.maintainers` block.

**`lib/packages.nix`** — pure Nix function that generates the README table from `config.packages`. Called via `legacyPackages.packagesTable`; the actual README markers are updated by `scripts/gen-packages-table.sh`.
Expand All @@ -41,6 +43,7 @@ Unfree vendor binaries are the exception: they live in `pkgs/default.nix`'s `unf
| Rust | `rustPlatform.buildRustPackage` | `cargoHash` in derivation |
| OCaml | `ocamlPackages.buildDunePackage` | — |
| Container image | `nix2container.buildImage` | `manifest.json` for pulled base images |
| oclif plugin | `mkSfPlugin` (`lib/salesforce`) | — |

## Adding a package

Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Cachix](https://img.shields.io/badge/cachix-unstoppablemango-blue)](https://unstoppablemango.cachix.org)
[![Last Commit](https://img.shields.io/github/last-commit/unmango/pkgs)](https://github.com/unmango/pkgs/commits/main)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![packages](https://img.shields.io/badge/packages-42-blue)](#packages)
[![packages](https://img.shields.io/badge/packages-44-blue)](#packages)

<p align="center">

Expand Down Expand Up @@ -57,6 +57,8 @@ Mini-nixpkgs of dubious quality.
| `pulumi-yaml` | Pulumi language host for YAML programs |
| `rust-analyzer-mcp` | Model Context Protocol (MCP) server that provides integration with rust-analyzer |
| `salesforce-cli` | CLI for developing against the Salesforce Platform |
| `sf-plugin-code-analyzer` | Salesforce Code Analyzer, a Salesforce CLI plugin for static analysis |
| `sfdx-git-delta` | Salesforce CLI plugin that generates a delta package from a git diff |
| `skopeo-nix2container` | Command line utility for various operations on container images and image repositories, with nix2container's nix: transport |
| `slackdump` | Save or export your private and public Slack messages, threads, files, and users locally without admin privileges |
| `terraform-plugin-codegen-framework` | Terraform Plugin Framework Code Generation |
Expand All @@ -82,6 +84,19 @@ nix run github:unmango/pkgs#kubectl-slice
nix shell github:unmango/pkgs#kube-vip
```

### Salesforce CLI plugins

`salesforce-cli.withPlugins` links plugins in as core plugins, so they come from
the store instead of `sf plugins install`'s mutable copy under
`$XDG_DATA_HOME/sf`.

```nix
pkgs.salesforce-cli.withPlugins [
pkgs.sfdx-git-delta
pkgs.sf-plugin-code-analyzer
]
```

### Overlay

```nix
Expand Down
10 changes: 10 additions & 0 deletions lib/salesforce/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{ pkgs }:
let
callPackage = pkgs.lib.callPackageWith (packages // pkgs);

packages = {
mkSfPlugin = callPackage ./plugin.nix { };
sfWithPlugins = callPackage ./with-plugins.nix { };
};
in
packages
Comment thread
UnstoppableMango marked this conversation as resolved.
93 changes: 93 additions & 0 deletions lib/salesforce/plugin.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
lib,
buildNpmPackage,
fetchurl,
jq,
}:

# Builds an oclif plugin published to the npm registry into
# $out/lib/node_modules/<npmName>, ready for `salesforce-cli.withPlugins`.
{
pname,
version,
# Registry name, e.g. "@salesforce/plugin-code-analyzer".
npmName,
# Hash of the registry tarball.
hash,
npmDepsHash,
# Packages a command of this plugin shells out to. `withPlugins` puts them on
# the PATH of the CLI it builds; a plugin derivation on its own has no bin to
# wrap, so this is the only place they can be attached.
runtimeInputs ? [ ],
passthru ? { },
...
}@args:
let
# Scoped names live under the scope on the registry but drop it from the
# tarball filename: @salesforce/plugin-x/-/plugin-x-1.0.0.tgz.
tarballName = lib.last (lib.splitString "/" npmName);
in
buildNpmPackage (
removeAttrs args [
"npmName"
"hash"
"runtimeInputs"
"passthru"
]
// {
inherit pname version npmDepsHash;

src = fetchurl {
url = "https://registry.npmjs.org/${npmName}/-/${tarballName}-${version}.tgz";
inherit hash;
};

npmDepsFetcherVersion = 2;

# Salesforce publishes plugins with an npm-shrinkwrap.json (plugin-trust
# signs it) whose dev subtree is incomplete, so `npm ci` falls back to the
# registry for the missing entries and fails in the sandbox. dist/ ships
# prebuilt, so drop the dev half of the lockfile and of package.json with
# it. jq is called by store path because buildNpmPackage replays postPatch
# inside fetchNpmDeps, whose build environment it does not control.
postPatch = ''
if [ -f npm-shrinkwrap.json ]; then
lockfile=npm-shrinkwrap.json
elif [ -f package-lock.json ]; then
lockfile=package-lock.json
else
echo "mkSfPlugin: ${npmName} ships no npm-shrinkwrap.json or package-lock.json" >&2
exit 1
fi

# The prune below walks .packages, which only exists from lockfileVersion
# 2 on. npm has written 2 or 3 since npm 7, so a version 1 lockfile is
# rejected rather than given a second pruning path.
lockfileVersion=$(${lib.getExe jq} '.lockfileVersion // 0' "$lockfile")
if [ "$lockfileVersion" -lt 2 ]; then
echo "mkSfPlugin: ${npmName} ships $lockfile at lockfileVersion $lockfileVersion, which has no .packages tree" >&2
exit 1
fi

${lib.getExe jq} 'del(.devDependencies)' package.json >patched.json
mv patched.json package.json

${lib.getExe jq} '
(.packages |= with_entries(select(.value.dev != true)))
| del(.packages[""].devDependencies)
' "$lockfile" >patched.json
Comment thread
coderabbitai[bot] marked this conversation as resolved.
mv patched.json "$lockfile"
'';

# dist/ ships prebuilt, and the install scripts are husky and telemetry.
npmFlags = [
"--ignore-scripts"
"--legacy-peer-deps"
];
dontNpmBuild = true;

passthru = passthru // {
inherit npmName runtimeInputs;
};
}
)
113 changes: 113 additions & 0 deletions lib/salesforce/with-plugins.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{
lib,
jq,
makeWrapper,
nodejs,
runCommand,
}:

# Rebuilds the Salesforce CLI with `plugins` linked in as core plugins, so they
# resolve from the store instead of from `sf plugins install`'s mutable copy
# under $XDG_DATA_HOME/sf.
let
sfWithPlugins =
{ salesforce-cli, plugins }:
let
runtimeInputs = lib.concatMap (plugin: plugin.runtimeInputs or [ ]) plugins;

wrapperArgs =
lib.mapAttrsToList (
name: value: "--set ${name} ${lib.escapeShellArg value}"
) salesforce-cli.runtimeEnv
++ lib.optional (runtimeInputs != [ ]) "--prefix PATH : ${lib.makeBinPath runtimeInputs}";
in
runCommand "salesforce-cli-${salesforce-cli.version}"
{
nativeBuildInputs = [
jq
makeWrapper
];

inherit plugins;
pluginNames = builtins.toJSON (map (plugin: plugin.npmName) plugins);
pluginDeps = builtins.toJSON (
lib.listToAttrs (lib.map (plugin: lib.nameValuePair plugin.npmName plugin.version) plugins)
);

inherit (salesforce-cli) pname version meta;

# Updates target pkgs/salesforce-cli/default.nix, so the base package's
# updateScript does not carry over to the composition.
passthru = removeAttrs salesforce-cli.passthru [ "updateScript" ] // {
inherit plugins;
# Stack onto what is already linked in rather than starting from the
# bare CLI again.
withPlugins =
extra:
sfWithPlugins {
inherit salesforce-cli;
plugins = plugins ++ extra;
};
};
}
''
upstream=${salesforce-cli}/lib/node_modules/@salesforce/cli
root=$out/lib/node_modules/@salesforce/cli
mkdir -p "$root/node_modules"

# The CLI's own ~2M of files are copied rather than symlinked: node resolves
# a module's dependencies from its realpath, so a symlinked entry point
# would look for node_modules beside the upstream copy and never see the
# plugins linked in below.
for entry in $(ls -A "$upstream"); do
if [ "$entry" != node_modules ]; then
cp -r --no-preserve=mode,ownership "$upstream/$entry" "$root/$entry"
fi
done

# Its 270M of dependencies stay symlinks. Scope directories are recreated as
# real directories so a scoped plugin can be linked in beside them.
link_into() {
local from="$1"
for entry in $(ls -A "$from"); do
case "$entry" in
@*)
mkdir -p "$root/node_modules/$entry"
for scoped in $(ls -A "$from/$entry"); do
ln -sfn "$from/$entry/$scoped" "$root/node_modules/$entry/$scoped"
done
;;
*)
ln -sfn "$from/$entry" "$root/node_modules/$entry"
;;
esac
done
}

link_into "$upstream/node_modules"
for plugin in $plugins; do
link_into "$plugin/lib/node_modules"
done

# oclif loads a core plugin only when it appears in both oclif.plugins and
# dependencies (see loadCorePlugins in @oclif/core), and a plugin that is
# also declared under oclif.jitPlugins would otherwise still be reported as
# not installed.
jq --argjson names "$pluginNames" --argjson deps "$pluginDeps" '
.oclif.plugins += $names
| .dependencies += $deps
| if .oclif.jitPlugins
then .oclif.jitPlugins |= with_entries(select(.key as $k | $names | index($k) | not))
else . end
' "$root/package.json" >package.json
mv package.json "$root/package.json"

mkdir -p $out/bin
for bin in sf sfdx; do
makeWrapper ${lib.getExe nodejs} "$out/bin/$bin" \
--add-flags "--no-deprecation $root/bin/run.js" \
${lib.concatStringsSep " " wrapperArgs}
done
'';
in
sfWithPlugins
3 changes: 3 additions & 0 deletions pkgs/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
inherit (inputs'.nix2container.packages) nix2container;
inherit (inputs'.gomod2nix.legacyPackages) buildGoApplication;
inherit (pkgs.callPackage ../lib/go { }) mkUpdateDeps;
inherit (pkgs.callPackage ../lib/salesforce { }) mkSfPlugin sfWithPlugins;
};

callPackage = lib.callPackageWith (tools // pkgs);
Expand Down Expand Up @@ -68,6 +69,8 @@
pulumi-yaml = callPackage ./pulumi-yaml { };
rust-analyzer-mcp = callPackage ./rust-analyzer-mcp { };
salesforce-cli = callPackage ./salesforce-cli { };
sf-plugin-code-analyzer = callPackage ./sf-plugin-code-analyzer { };
sfdx-git-delta = callPackage ./sfdx-git-delta { };
skopeo-nix2container = callPackage ./skopeo-nix2container {
inherit (inputs'.nix2container.packages) skopeo-nix2container;
};
Expand Down
Loading