Skip to content

Commit 33c3f8c

Browse files
fix: errors are shown on postinstall when using sudo
When `sudo npm i -g nativescript` is used, postinstall tasks produce errors, which confuse the users. CLI is actually installed successfully, but error for EACCESS is often shown. The problem is that npm executes the postinstall scripts with a special OS user - `nobody`. This user does not have permissions to write in users' directories, so generating html help or writing the user-settings.json file fails. Npm's solution is to pass `--unsafe-perm` to the `sudo npm install -g nativescript` command, which forces the postinstall to be executed with root user. Fix CLI's code to skip postinstall tasks in case sudo is used and `--unsafe-perm` is not passed.
1 parent 70ee4af commit 33c3f8c

File tree

2 files changed

+85
-6
lines changed

2 files changed

+85
-6
lines changed

lib/commands/post-install.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { doesCurrentNpmCommandMatch } from "../common/helpers";
2+
13
export class PostInstallCliCommand implements ICommand {
24
constructor(private $fs: IFileSystem,
35
private $subscriptionService: ISubscriptionService,
@@ -13,24 +15,35 @@ export class PostInstallCliCommand implements ICommand {
1315
public allowedParameters: ICommandParameter[] = [];
1416

1517
public async execute(args: string[]): Promise<void> {
18+
const isRunningWithSudoUser = !!process.env.SUDO_USER;
19+
1620
if (!this.$hostInfo.isWindows) {
1721
// when running under 'sudo' we create a working dir with wrong owner (root) and
1822
// it is no longer accessible for the user initiating the installation
1923
// patch the owner here
20-
if (process.env.SUDO_USER) {
24+
if (isRunningWithSudoUser) {
2125
// TODO: Check if this is the correct place, probably we should set this at the end of the command.
2226
await this.$fs.setCurrentUserAsOwner(this.$settingsService.getProfileDir(), process.env.SUDO_USER);
2327
}
2428
}
2529

26-
await this.$helpService.generateHtmlPages();
27-
// Explicitly ask for confirmation of usage-reporting:
28-
await this.$analyticsService.checkConsent();
29-
await this.$commandsService.tryExecuteCommand("autocomplete", []);
30+
const canExecutePostInstallTask = !isRunningWithSudoUser || doesCurrentNpmCommandMatch([/^--unsafe-perm$/]);
31+
32+
if (canExecutePostInstallTask) {
33+
await this.$helpService.generateHtmlPages();
34+
35+
// Explicitly ask for confirmation of usage-reporting:
36+
await this.$analyticsService.checkConsent();
37+
await this.$commandsService.tryExecuteCommand("autocomplete", []);
38+
}
39+
3040
// Make sure the success message is separated with at least one line from all other messages.
3141
this.$logger.out();
3242
this.$logger.printMarkdown("Installation successful. You are good to go. Connect with us on `http://twitter.com/NativeScript`.");
33-
await this.$subscriptionService.subscribeForNewsletter();
43+
44+
if (canExecutePostInstallTask) {
45+
await this.$subscriptionService.subscribeForNewsletter();
46+
}
3447
}
3548

3649
public async postCommandAction(args: string[]): Promise<void> {

test/commands/post-install.ts

+66
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ const createTestInjector = (): IInjector => {
4949
};
5050

5151
describe("post-install command", () => {
52+
const originalNpmArgv = process.env.npm_config_argv;
53+
const originalSudoUser = process.env.SUDO_USER;
54+
55+
afterEach(() => {
56+
process.env.npm_config_argv = originalNpmArgv;
57+
process.env.SUDO_USER = originalSudoUser;
58+
});
59+
5260
it("calls subscriptionService.subscribeForNewsletter method", async () => {
5361
const testInjector = createTestInjector();
5462
const subscriptionService = testInjector.resolve<ISubscriptionService>("subscriptionService");
@@ -61,4 +69,62 @@ describe("post-install command", () => {
6169
await postInstallCommand.execute([]);
6270
assert.isTrue(isSubscribeForNewsletterCalled, "post-install-cli command must call subscriptionService.subscribeForNewsletter");
6371
});
72+
73+
const verifyResult = async (opts: { shouldCallMethod: boolean }): Promise<void> => {
74+
const testInjector = createTestInjector();
75+
const subscriptionService = testInjector.resolve<ISubscriptionService>("subscriptionService");
76+
let isSubscribeForNewsletterCalled = false;
77+
subscriptionService.subscribeForNewsletter = async (): Promise<void> => {
78+
isSubscribeForNewsletterCalled = true;
79+
};
80+
81+
const helpService = testInjector.resolve<IHelpService>("helpService");
82+
let isGenerateHtmlPagesCalled = false;
83+
helpService.generateHtmlPages = async (): Promise<void> => {
84+
isGenerateHtmlPagesCalled = true;
85+
};
86+
87+
const analyticsService = testInjector.resolve<IAnalyticsService>("analyticsService");
88+
let isCheckConsentCalled = false;
89+
analyticsService.checkConsent = async (): Promise<void> => {
90+
isCheckConsentCalled = true;
91+
};
92+
93+
const commandsService = testInjector.resolve<ICommandsService>("commandsService");
94+
let isTryExecuteCommandCalled = false;
95+
commandsService.tryExecuteCommand = async (): Promise<void> => {
96+
isTryExecuteCommandCalled = true;
97+
};
98+
99+
const postInstallCommand = testInjector.resolveCommand("post-install-cli");
100+
await postInstallCommand.execute([]);
101+
102+
process.env.npm_config_argv = originalNpmArgv;
103+
process.env.SUDO_USER = originalSudoUser;
104+
105+
const hasNotInMsg = opts.shouldCallMethod ? "" : "NOT";
106+
107+
assert.equal(isSubscribeForNewsletterCalled, opts.shouldCallMethod, `post-install-cli command must ${hasNotInMsg} call subscriptionService.subscribeForNewsletter`);
108+
assert.equal(isGenerateHtmlPagesCalled, opts.shouldCallMethod, `post-install-cli command must ${hasNotInMsg} call helpService.generateHtmlPages`);
109+
assert.equal(isCheckConsentCalled, opts.shouldCallMethod, `post-install-cli command must ${hasNotInMsg} call analyticsService.checkConsent`);
110+
assert.equal(isTryExecuteCommandCalled, opts.shouldCallMethod, `post-install-cli command must ${hasNotInMsg} call commandsService.tryExecuteCommand`);
111+
};
112+
113+
it("does not call specific methods when CLI is installed with sudo without `--unsafe-perm`", () => {
114+
process.env.npm_config_argv = JSON.stringify({});
115+
process.env.SUDO_USER = "user1";
116+
return verifyResult({ shouldCallMethod: false });
117+
});
118+
119+
it("calls specific methods when CLI is installed with sudo with `--unsafe-perm`", async () => {
120+
process.env.npm_config_argv = JSON.stringify({ original: ["--unsafe-perm"] });
121+
process.env.SUDO_USER = "user1";
122+
return verifyResult({ shouldCallMethod: true });
123+
});
124+
125+
it("calls specific methods when CLI is installed without sudo", async () => {
126+
process.env.npm_config_argv = JSON.stringify({});
127+
delete process.env.SUDO_USER;
128+
return verifyResult({ shouldCallMethod: true });
129+
});
64130
});

0 commit comments

Comments
 (0)