Skip to content

Commit

Permalink
feat(link): generate shims for missing 'bin' scripts (#2059)
Browse files Browse the repository at this point in the history
During the 'link' step, if a package's binary targets don't exist yet they will fail to be symlinked, even if a later `run xxx` step would have generated them. This is a situation that occurs in monorepos containing build tools compiled from another language (e.g. TypeScript).

Simply adding the symlinks whether the target exists or not is not good enough, as the symlink target needs to be `chmod +x`ed, which is lerna's/npm's responsibility.

Instead, if the target doesn't exist, generate a shim shell script that will `chmod` and `exec` the intended target, similar to what would happen on Windows normally.

Fixes #1444
  • Loading branch information
rix0rrr authored and evocateur committed May 11, 2019
1 parent 6eca172 commit 90acdde
Show file tree
Hide file tree
Showing 10 changed files with 80 additions and 11 deletions.
@@ -0,0 +1 @@
a-build-tool.js
@@ -0,0 +1,3 @@
{
"version": "1.0.0"
}
@@ -0,0 +1,7 @@
{
"name": "basic",
"version": "monorepo",
"private": true,
"dependencies": {
}
}
@@ -0,0 +1,10 @@
{
"name": "@test/build-tool",
"version": "1.0.0",
"bin": {
"a-build-tool": "a-build-tool.js"
},
"scripts": {
"build": "echo \"#!/usr/bin/env node\nconsole.log('build tool executed')\" > a-build-tool.js"
}
}
@@ -0,0 +1,10 @@
{
"name": "@test/buildable",
"version": "2.0.0",
"devDependencies": {
"@test/build-tool": "*"
},
"scripts": {
"build": "a-build-tool"
}
}
16 changes: 16 additions & 0 deletions integration/lerna-link-sibling-bins.test.js
@@ -0,0 +1,16 @@
"use strict";

const cliRunner = require("@lerna-test/cli-runner");
const initFixture = require("@lerna-test/init-fixture")(__dirname);

test("lerna link symlinks generated binaries of sibling packages", async () => {
const cwd = await initFixture("lerna-generated-build-tool");
const lerna = cliRunner(cwd);

// First bootstrap, I expect this to succeed but don't are about the output
await lerna("bootstrap");

const { stdout } = await lerna("run", "build");

expect(stdout).toMatch("build tool executed");
});
1 change: 1 addition & 0 deletions utils/create-symlink/__tests__/create-symlink.test.js
Expand Up @@ -15,6 +15,7 @@ describe("create-symlink", () => {
fs.lstat.mockImplementation(() => Promise.reject(new Error("MOCK")));
fs.unlink.mockResolvedValue();
fs.symlink.mockResolvedValue();
fs.pathExists.mockResolvedValue(true);
// cmdShim is a traditional errback
cmdShim.mockImplementation(callsBack());

Expand Down
25 changes: 23 additions & 2 deletions utils/create-symlink/create-symlink.js
Expand Up @@ -31,9 +31,20 @@ function createSymbolicLink(src, dest, type) {

function createPosixSymlink(origin, dest, _type) {
const type = _type === "exec" ? "file" : _type;
const src = path.relative(path.dirname(dest), origin);
const relativeSymlink = path.relative(path.dirname(dest), origin);

return createSymbolicLink(src, dest, type);
if (_type === "exec") {
// If the target exists, create real symlink. If the target doesn't exist yet,
// create a shim shell script.
return fs.pathExists(origin).then(exists => {
if (exists) {
return createSymbolicLink(relativeSymlink, dest, type).then(() => fs.chmod(origin, "755"));
}
return shShim(origin, dest, type).then(() => fs.chmod(dest, "755"));
});
}

return createSymbolicLink(relativeSymlink, dest, type);
}

function createWindowsSymlink(src, dest, type) {
Expand All @@ -51,3 +62,13 @@ function createWindowsSymlink(src, dest, type) {

return createSymbolicLink(src, dest, type);
}

function shShim(target, script, type) {
log.silly("shShim", [target, script, type]);

const absTarget = path.resolve(path.dirname(script), target);

const scriptLines = ["#!/bin/sh", `chmod +x ${absTarget} && exec ${absTarget} "$@"`];

return fs.writeFile(script, scriptLines.join("\n"));
}
6 changes: 3 additions & 3 deletions utils/symlink-binary/__tests__/symlink-binary.test.js
Expand Up @@ -42,15 +42,15 @@ describe("symlink-binary", () => {
expect(dstPath).not.toHaveBinaryLinks();
});

it("should skip missing bin files", async () => {
it("should create shims for all declared binaries", async () => {
const testDir = await initFixture("links");
const srcPath = path.join(testDir, "packages/package-3");
const dstPath = path.join(testDir, "packages/package-4");

await symlinkBinary(srcPath, dstPath);

expect(srcPath).toHaveExecutables("cli1.js", "cli2.js");
expect(dstPath).toHaveBinaryLinks("links3cli1", "links3cli2");
expect(dstPath).toHaveBinaryLinks("links3cli1", "links3cli2", "links3cli3");
});

it("should preserve previous bin entries", async () => {
Expand All @@ -62,6 +62,6 @@ describe("symlink-binary", () => {
await symlinkBinary(pkg2Path, destPath);
await symlinkBinary(pkg3Path, destPath);

expect(destPath).toHaveBinaryLinks("links-2", "links3cli1", "links3cli2");
expect(destPath).toHaveBinaryLinks("links-2", "links3cli1", "links3cli2", "links3cli3");
});
});
12 changes: 6 additions & 6 deletions utils/symlink-binary/symlink-binary.js
Expand Up @@ -22,11 +22,11 @@ function symlinkBinary(srcPackageRef, destPackageRef) {
const src = path.join(srcPackage.location, srcPackage.bin[name]);
const dst = path.join(destPackage.binLocation, name);

return fs.pathExists(src).then(exists => {
if (exists) {
return { src, dst };
}
});
// Symlink all declared binaries, even if they don't exist (yet). We will
// assume the package author knows what they're doing and that the binaries
// will be generated during a later build phase (potentially source compiled from
// another language).
return { src, dst };
});

if (actions.length === 0) {
Expand All @@ -36,7 +36,7 @@ function symlinkBinary(srcPackageRef, destPackageRef) {
return fs.mkdirp(destPackage.binLocation).then(() =>
pMap(actions, meta => {
if (meta) {
return createSymlink(meta.src, meta.dst, "exec").then(() => fs.chmod(meta.src, "755"));
return createSymlink(meta.src, meta.dst, "exec");
}
})
);
Expand Down

0 comments on commit 90acdde

Please sign in to comment.