Skip to content

Commit

Permalink
Address PR comments
Browse files Browse the repository at this point in the history
  • Loading branch information
alexdavid committed Nov 30, 2023
1 parent a93dc26 commit 61a979f
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 36 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

- Add `garn.javascript.prettier` plugin to automatically format and check
javascript formatting using [prettier](https://prettier.io/).
- Add `garn.importFlake` helper to allow extending a flake file by local path
or URL.

## v0.0.18

Expand Down
62 changes: 51 additions & 11 deletions ts/importFlake.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ describe("importFlake", () => {
});

const writeFlakeToImport = (contents: string) => {
try {
Deno.mkdirSync(`${tempDir}/to-import`);
} catch (err) {
if (!(err instanceof Deno.errors.AlreadyExists)) throw err;
}
Deno.mkdirSync(`${tempDir}/to-import`, { recursive: true });
Deno.writeTextFileSync(
`${tempDir}/to-import/flake.nix`,
`
Expand Down Expand Up @@ -79,7 +75,7 @@ describe("importFlake", () => {
packages = {
main_pkg = pkgs.runCommand "create-some-files" {} ''
mkdir $out
touch $out/foo $out/bar
touch $out/original
'';
};
}`);
Expand All @@ -90,12 +86,12 @@ describe("importFlake", () => {
packages = {
main_pkg = pkgs.runCommand "create-some-files" {} ''
mkdir $out
touch $out/baz
touch $out/modified
'';
};
}`);
const output = assertSuccess(runExecutable(exe, { cwd: tempDir }));
assertStdout(output, "baz\n");
assertStdout(output, "modified\n");
});

it("allows importing relative sources in packages", () => {
Expand All @@ -115,6 +111,17 @@ describe("importFlake", () => {
const output = assertSuccess(runExecutable(exe, { cwd: tempDir }));
assertStdout(output, "Hello from a js file in tempDir!\n");
});

it("displays a helpful error if the specified package does not exist", () => {
writeFlakeToImport(`{ packages = { }; }`);
const flake = importFlake("./to-import");
const exe = garn.shell`${flake.getPackage("foo")}`;
const output = runExecutable(exe, { cwd: tempDir });
assertStderrContains(
output,
'error: The package "foo" was not found in ./to-import',
);
});
});

describe("getApp", () => {
Expand All @@ -134,6 +141,17 @@ describe("importFlake", () => {
const output = assertSuccess(runExecutable(exe, { cwd: tempDir }));
assertStdout(output, "hello from flake file\n");
});

it("displays a helpful error if the specified app does not exist", () => {
writeFlakeToImport(`{ apps = { }; }`);
const flake = importFlake("./to-import");
const exe = garn.shell`${flake.getApp("foo")}`;
const output = runExecutable(exe, { cwd: tempDir });
assertStderrContains(
output,
'error: The app "foo" was not found in ./to-import',
);
});
});

describe("getCheck", () => {
Expand All @@ -152,14 +170,25 @@ describe("importFlake", () => {
const output = assertSuccess(runCheck(check, { dir: tempDir }));
assertStderrContains(output, "running my-check!");
});

it("displays a helpful error if the specified check does not exist", () => {
writeFlakeToImport(`{ checks = { }; }`);
const flake = importFlake("./to-import");
const exe = garn.shell`${flake.getCheck("foo")}`;
const output = runExecutable(exe, { cwd: tempDir });
assertStderrContains(
output,
'error: The check "foo" was not found in ./to-import',
);
});
});

describe("getDevShell", () => {
it("returns the specified environment from the flake file", () => {
writeFlakeToImport(`{
devShells = {
some-shell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ hello ];
nativeBuildInputs = [ pkgs.hello ];
};
};
}`);
Expand All @@ -168,14 +197,25 @@ describe("importFlake", () => {
const output = enterEnvironment(env, ["hello"], { dir: tempDir });
assertStdout(output, "Hello, world!\n");
});

it("displays a helpful error if the specified devShell does not exist", () => {
writeFlakeToImport(`{ devShells = { }; }`);
const flake = importFlake("./to-import");
const exe = garn.shell`${flake.getDevShell("foo")}`;
const output = runExecutable(exe, { cwd: tempDir });
assertStderrContains(
output,
'error: The devShell "foo" was not found in ./to-import',
);
});
});

describe("allPackages", () => {
it("returns a package with symlinks to all found packages", () => {
writeFlakeToImport(`{
packages = {
foo = pkgs.runCommand "create-foo" {} "echo foo-pkg > $out";
bar = pkgs.runCommand "create-bar" {} "echo bar-pkg > $out";
foo = pkgs.runCommand "foo" {} "echo foo-pkg > $out";
bar = pkgs.runCommand "bar" {} "echo bar-pkg > $out";
};
}`);
const flake = importFlake("./to-import");
Expand Down
55 changes: 38 additions & 17 deletions ts/importFlake.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
import { Check } from "./check.ts";
import { Environment, mkEnvironment } from "./environment.ts";
import { Executable, mkExecutable } from "./executable.ts";
import { NixExpression, nixFlakeDep, nixRaw, nixStrLit } from "./nix.ts";
import {
getPathOrError,
NixExpression,
nixFlakeDep,
nixRaw,
nixStrLit,
} from "./nix.ts";
import { mkPackage, Package } from "./package.ts";

function getFlake(flakeDir: string): NixExpression {
if (flakeDir.match(/^[.][.]?[/]/)) {
function getFlake(flakeUrl: string): NixExpression {
if (flakeUrl.match(/^[.][.]?[/]/)) {
const callFlake = nixFlakeDep("call-flake", {
url: "github:divnix/call-flake",
});
return nixRaw`(${callFlake} ${nixRaw(flakeDir)})`;
return nixRaw`(${callFlake} ${nixRaw(flakeUrl)})`;
}
return nixFlakeDep(flakeDir.replaceAll(/[^a-zA-Z0-9]/g, "-"), {
url: flakeDir,
return nixFlakeDep(flakeUrl.replaceAll(/[^a-zA-Z0-9]/g, "-"), {
url: flakeUrl,
});
}

export function importFlake(flakeDir: string): {
export function importFlake(flakeUrl: string): {
allChecks: Check;
allPackages: Package;
getApp: (appName: string) => Executable;
getCheck: (appName: string) => Check;
getDevShell: (devShellName: string) => Environment;
getPackage: (packageName: string) => Package;
} {
const flake = getFlake(flakeDir);
const flake = getFlake(flakeUrl);

return {
allChecks: {
tag: "check",
Expand All @@ -33,31 +40,45 @@ export function importFlake(flakeDir: string): {

allPackages: mkPackage(
nixRaw`pkgs.linkFarm "all-packages" ${flake}.packages.\${system}`,
`All packages from flake.nix in ${flakeDir}`,
`All packages from flake.nix in ${flakeUrl}`,
),

getApp: (appName) =>
mkExecutable(
nixRaw`${flake}.apps.\${system}.${nixStrLit(appName)}.program`,
`Execute ${appName} from flake.nix in ${flakeDir}`,
getPathOrError(
flake,
["apps", nixRaw`\${system}`, appName, "program"],
nixStrLit`The app "${appName}" was not found in ${flakeUrl}`,
),
`Execute ${appName} from flake.nix in ${flakeUrl}`,
),

getCheck: (checkName) => ({
tag: "check",
nixExpression: nixRaw`${flake}.checks.\${system}.${nixStrLit(checkName)}`,
nixExpression: getPathOrError(
flake,
["checks", nixRaw`\${system}`, checkName],
nixStrLit`The check "${checkName}" was not found in ${flakeUrl}`,
),
}),

getDevShell: (devShellName) =>
mkEnvironment({
nixExpression: nixRaw`${flake}.devShells.\${system}.${nixStrLit(
devShellName,
)}`,
nixExpression: getPathOrError(
flake,
["devShells", nixRaw`\${system}`, devShellName],
nixStrLit`The devShell "${devShellName}" was not found in ${flakeUrl}`,
),
}),

getPackage: (packageName) =>
mkPackage(
nixRaw`${flake}.packages.\${system}.${nixStrLit(packageName)}`,
`${packageName} from flake.nix in ${flakeDir}`,
getPathOrError(
flake,
["packages", nixRaw`\${system}`, packageName],
nixStrLit`The package "${packageName}" was not found in ${flakeUrl}`,
),
`${packageName} from flake.nix in ${flakeUrl}`,
),
};
}
7 changes: 7 additions & 0 deletions ts/internal/interpolatedString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,10 @@ export function getInterpolations<T>(
): Array<T> {
return interpolated.rest.map(([node, _str]) => node);
}

export function join<T>(arr: Array<T>, joiner: string): InterpolatedString<T> {
return {
initial: "",
rest: arr.map((el, idx) => [el, idx < arr.length - 1 ? joiner : ""]),
};
}
14 changes: 7 additions & 7 deletions ts/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ export const mapKeys = <T>(
return result;
};

export const mapValues = <T extends Record<string, unknown>, U>(
f: (i: T[keyof T], key: keyof T) => U,
x: T,
): { [key in keyof T]: U } => {
const result: Partial<{ [key in keyof T]: U }> = {};
export const mapValues = <Obj extends Record<string, unknown>, FnResult>(
f: (i: Obj[keyof Obj], key: keyof Obj) => FnResult,
x: Obj,
): { [key in keyof Obj]: FnResult } => {
const result: Partial<{ [key in keyof Obj]: FnResult }> = {};
for (const [key, value] of Object.entries(x) as Array<
[keyof T, T[keyof T]]
[keyof Obj, Obj[keyof Obj]]
>) {
result[key] = f(value, key);
}
return result as { [key in keyof T]: U };
return result as { [key in keyof Obj]: FnResult };
};

export const filterNullValues = <T>(
Expand Down
24 changes: 24 additions & 0 deletions ts/nix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
InterpolatedString,
interpolatedStringFromString,
interpolatedStringFromTemplate,
join,
mapStrings,
renderInterpolatedString,
} from "./internal/interpolatedString.ts";
Expand Down Expand Up @@ -210,6 +211,29 @@ export function escapeShellArg(shellArg: NixExpression): NixExpression {
return nixRaw`(pkgs.lib.strings.escapeShellArg ${shellArg})`;
}

export function getPathOrError(
attrSet: NixExpression,
path: Array<NixExpression | string>,
error: NixExpression,
): NixExpression {
const pathExpr: NixExpression = {
[__nixExpressionTag]: null,
type: "raw",
raw: join(
path.map((el) => (typeof el === "string" ? nixStrLit(el) : el)),
".",
),
};
return nixRaw`
let
x = ${attrSet};
in
if x ? ${pathExpr}
then x.${pathExpr}
else builtins.throw ${error}
`;
}

/**
* Returns a `NixExpression` that renders as an identifier that refers to a
* flake input. At the same time it registers the flake input as a dependency,
Expand Down
5 changes: 4 additions & 1 deletion ts/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export type Package = {
setDescription: (this: Package, newDescription: string) => Package;

/**
* Get an executable by name within the `bin` directory of this `Package`
* Get an executable by name within the `bin` directory of this `Package`.
*
* If that executable doesn't exist this will only fail when trying to *run*
* the `Executable`, not before.
*/
bin: (executableName: string) => Executable;
};
Expand Down

0 comments on commit 61a979f

Please sign in to comment.