NodeJsInstaller::createBinScripts()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
cc 4
nc 5
nop 3
1
<?php
2
namespace Mouf\NodeJsInstaller;
3
4
use Composer\Composer;
5
use Composer\IO\IOInterface;
6
use Composer\Util\RemoteFilesystem;
7
8
class NodeJsInstaller
9
{
10
11
    /**
12
     * @var IOInterface
13
     */
14
    private $io;
15
16
    protected $rfs;
17
18
    public function __construct(IOInterface $io, Composer $composer)
19
    {
20
        $this->io = $io;
21
        $this->rfs = new RemoteFilesystem($io, $composer->getConfig());
22
    }
23
24
    /**
25
     * Checks if NodeJS is installed globally.
26
     * If yes, will return the version number.
27
     * If no, will return null.
28
     *
29
     * Note: trailing "v" will be removed from version string.
30
     *
31
     * @return null|string
32
     */
33
    public function getNodeJsGlobalInstallVersion()
34
    {
35
        $returnCode = 0;
36
        $output = "";
37
38
        ob_start();
39
        $version = exec("nodejs -v 2>&1", $output, $returnCode);
40
        ob_end_clean();
41
42
        if ($returnCode !== 0) {
43
            ob_start();
44
            $version = exec("node -v 2>&1", $output, $returnCode);
45
            ob_end_clean();
46
47
            if ($returnCode !== 0) {
48
                return;
49
            }
50
        }
51
52
        return ltrim($version, "v");
53
    }
54
55
    /**
56
     * Returns the full path to NodeJS global install (if available).
57
     */
58
    public function getNodeJsGlobalInstallPath()
59
    {
60
        $pathToNodeJS = $this->getGlobalInstallPath("nodejs");
61
        if (!$pathToNodeJS) {
62
            $pathToNodeJS = $this->getGlobalInstallPath("node");
63
        }
64
65
        return $pathToNodeJS;
66
    }
67
68
    /**
69
     * Returns the full install path to a command
70
     * @param string $command
71
     */
72
    public function getGlobalInstallPath($command)
73
    {
74
        if (Environment::isWindows()) {
75
            $result = trim(shell_exec("where /F ".escapeshellarg($command)), "\n\r");
76
77
            // "Where" can return several lines.
78
            $lines = explode("\n", $result);
79
80
            return $lines[0];
81
        } else {
82
            // We want to get output from stdout, not from stderr.
83
            // Therefore, we use proc_open.
84
            $descriptorspec = array(
85
                0 => array("pipe", "r"),  // stdin
86
                1 => array("pipe", "w"),  // stdout
87
                2 => array("pipe", "w"),  // stderr
88
            );
89
            $pipes = array();
90
91
            $process = proc_open("which ".escapeshellarg($command), $descriptorspec, $pipes);
92
93
            $stdout = stream_get_contents($pipes[1]);
94
            fclose($pipes[1]);
95
96
            // Let's ignore stderr (it is possible we do not find anything and depending on the OS, stderr will
97
            // return things or not)
98
            fclose($pipes[2]);
99
100
            proc_close($process);
101
102
            return trim($stdout, "\n\r");
103
        }
104
    }
105
106
    /**
107
     * Checks if NodeJS is installed locally.
108
     * If yes, will return the version number.
109
     * If no, will return null.
110
     *
111
     * Note: trailing "v" will be removed from version string.
112
     *
113
     * @return null|string
114
     */
115
    public function getNodeJsLocalInstallVersion($binDir)
116
    {
117
        $returnCode = 0;
118
        $output = "";
119
120
        $cwd = getcwd();
121
        chdir(__DIR__.'/../../../../');
122
123
        ob_start();
124
125
        $version = exec($binDir.DIRECTORY_SEPARATOR.'node -v 2>&1', $output, $returnCode);
126
127
        ob_end_clean();
128
129
        chdir($cwd);
130
131
        if ($returnCode !== 0) {
132
            return;
133
        } else {
134
            return ltrim($version, "v");
135
        }
136
    }
137
138
    /**
139
     * Returns URL based on version.
140
     * URL is dependent on environment
141
     * @param  string                   $version
142
     * @return string
143
     * @throws NodeJsInstallerException
144
     */
145
    public function getNodeJSUrl($version)
146
    {
147
        if (Environment::isWindows() && Environment::getArchitecture() == 32) {
148 View Code Duplication
            if (version_compare($version, '4.0.0') >= 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
149
                return "https://nodejs.org/dist/v".$version."/win-x86/node.exe";
150
            } else {
151
                return "https://nodejs.org/dist/v".$version."/node.exe";
152
            }
153
        } elseif (Environment::isWindows() && Environment::getArchitecture() == 64) {
154 View Code Duplication
            if (version_compare($version, '4.0.0') >= 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
155
                return "https://nodejs.org/dist/v" . $version . "/win-x64/node.exe";
156
            } else {
157
                return "https://nodejs.org/dist/v" . $version . "/x64/node.exe";
158
            }
159
        } elseif (Environment::isMacOS() && Environment::getArchitecture() == 32) {
160
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-darwin-x86.tar.gz";
161
        } elseif (Environment::isMacOS() && Environment::getArchitecture() == 64) {
162
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-darwin-x64.tar.gz";
163
        } elseif (Environment::isSunOS() && Environment::getArchitecture() == 32) {
164
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-sunos-x86.tar.gz";
165
        } elseif (Environment::isSunOS() && Environment::getArchitecture() == 64) {
166
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-sunos-x64.tar.gz";
167
        } elseif (Environment::isLinux() && Environment::isArm()) {
168
            if (version_compare($version, '4.0.0') >= 0) {
169
                if (Environment::isArmV6l()) {
170
                    return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-armv6l.tar.gz";
171
                } elseif (Environment::isArmV7l()) {
172
                    return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-armv7l.tar.gz";
173
                } elseif (Environment::getArchitecture() == 64) {
174
                    return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-arm64.tar.gz";
175
                } else {
176
                    throw new NodeJsInstallerException('NodeJS-installer cannot install Node on computers with ARM 32bits processors that are not v6l or v7l. Please install NodeJS globally on your machine first, then run composer again.');
177
                }
178
            } else {
179
                throw new NodeJsInstallerException('NodeJS-installer cannot install Node <4.0 on computers with ARM processors. Please install NodeJS globally on your machine first, then run composer again, or consider installing a version of NodeJS >=4.0.');
180
            }
181
        } elseif (Environment::isLinux() && Environment::getArchitecture() == 32) {
182
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-x86.tar.gz";
183
        } elseif (Environment::isLinux() && Environment::getArchitecture() == 64) {
184
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-x64.tar.gz";
185
        } else {
186
            throw new NodeJsInstallerException('Unsupported architecture: '.PHP_OS.' - '.Environment::getArchitecture().' bits');
187
        }
188
    }
189
190
    /**
191
     * Installs NodeJS
192
     * @param  string                   $version
193
     * @param  string                   $targetDirectory
194
     * @throws NodeJsInstallerException
195
     */
196
    public function install($version, $targetDirectory)
197
    {
198
        $this->io->write("Installing <info>NodeJS v".$version."</info>");
199
        $url = $this->getNodeJSUrl($version);
200
        $this->io->write("  Downloading from $url");
201
202
        $cwd = getcwd();
203
204
        $fileName = 'vendor/'.pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_BASENAME);
205
206
        $this->rfs->copy(parse_url($url, PHP_URL_HOST), $url, $fileName);
0 ignored issues
show
Security Bug introduced by
It seems like parse_url($url, PHP_URL_HOST) targeting parse_url() can also be of type false; however, Composer\Util\RemoteFilesystem::copy() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
207
208
        if (!file_exists($fileName)) {
209
            throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
210
                .' directory is writable and you have internet connectivity');
211
        }
212
213
        if (!file_exists($targetDirectory)) {
214
            mkdir($targetDirectory, 0775, true);
215
        }
216
217
        if (!is_writable($targetDirectory)) {
218
            throw new NodeJsInstallerException("'$targetDirectory' is not writable");
219
        }
220
221
        if (!Environment::isWindows()) {
222
            // Now, if we are not in Windows, let's untar.
223
            $this->extractTo($fileName, $targetDirectory);
224
225
            // Let's delete the downloaded file.
226
            unlink($fileName);
227
        } else {
228
            // If we are in Windows, let's move and install NPM.
229
            rename($fileName, $targetDirectory.'/'.basename($fileName));
230
231
            // We have to download the latest available version in a bin for Windows, then upgrade it:
232
            $url = "https://nodejs.org/dist/npm/npm-1.4.12.zip";
233
            $npmFileName = "vendor/npm-1.4.12.zip";
234
            $this->rfs->copy(parse_url($url, PHP_URL_HOST), $url, $npmFileName);
0 ignored issues
show
Security Bug introduced by
It seems like parse_url($url, PHP_URL_HOST) targeting parse_url() can also be of type false; however, Composer\Util\RemoteFilesystem::copy() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
235
236
            $this->unzip($npmFileName, $targetDirectory);
237
238
            unlink($npmFileName);
239
240
            // Let's update NPM
241
            $highestNpmVersion = "latest";
242
            if ($version < 10) {
243
                $highestNpmVersion = "6.14.8";
244
            }
245
246
            // 1- Update PATH to run npm.
247
            $path = getenv('PATH');
248
            $newPath = realpath($targetDirectory).";".$path;
249
            putenv('PATH='.$newPath);
250
251
            // 2- Run npm
252
            $cwd2 = getcwd();
253
            chdir($targetDirectory);
254
255
            $returnCode = 0;
256
            passthru("npm update npm@{$highestNpmVersion}", $returnCode);
257
            if ($returnCode !== 0) {
258
                throw new NodeJsInstallerException("An error occurred while updating NPM to latest version.");
259
            }
260
261
            // Finally, let's copy the base npm file for Cygwin
262
            if (file_exists('node_modules/npm/bin/npm')) {
263
                copy('node_modules/npm/bin/npm', 'npm');
264
            }
265
266
            chdir($cwd2);
267
        }
268
269
        chdir($cwd);
270
    }
271
272
    /**
273
     * Extract tar.gz file to target directory.
274
     *
275
     * @param string $tarGzFile
276
     * @param string $targetDir
277
     */
278
    private function extractTo($tarGzFile, $targetDir)
279
    {
280
        // Note: we cannot use PharData class because it does not keeps symbolic links.
281
        // Also, --strip 1 allows us to remove the first directory.
282
283
        $output = $return_var = null;
284
285
        exec("tar -xvf ".$tarGzFile." -C ".escapeshellarg($targetDir)." --strip 1", $output, $return_var);
286
287
        if ($return_var !== 0) {
288
            throw new NodeJsInstallerException("An error occurred while untaring NodeJS ($tarGzFile) to $targetDir");
289
        }
290
    }
291
292
    public function createBinScripts($binDir, $targetDir, $isLocal)
293
    {
294
        if (!file_exists($binDir)) {
295
            $result = mkdir($binDir, 0775, true);
296
            if ($result === false) {
297
                throw new NodeJsInstallerException("Unable to create directory ".$binDir);
298
            }
299
        }
300
301
        $fullTargetDir = realpath($targetDir);
302
        $binDir = realpath($binDir);
303
304
        if (!Environment::isWindows()) {
305
            $this->createBinScript($binDir, $fullTargetDir, 'node', 'node', $isLocal);
306
            $this->createBinScript($binDir, $fullTargetDir, 'npm', 'npm', $isLocal);
307
        } else {
308
            $this->createBinScript($binDir, $fullTargetDir, 'node.bat', 'node', $isLocal);
309
            $this->createBinScript($binDir, $fullTargetDir, 'npm.bat', 'npm', $isLocal);
310
        }
311
    }
312
313
    /**
314
     * Copy script into $binDir, replacing PATH with $fullTargetDir
315
     * @param string $binDir
316
     * @param string $fullTargetDir
317
     * @param string $scriptName
318
     * @param bool   $isLocal
319
     */
320
    private function createBinScript($binDir, $fullTargetDir, $scriptName, $target, $isLocal)
321
    {
322
        $content = file_get_contents(__DIR__.'/../bin/'.($isLocal ? "local/" : "global/").$scriptName);
323
        if ($isLocal) {
324
            $path = $this->makePathRelative($fullTargetDir, $binDir);
325
        } else {
326
            if ($scriptName == "node") {
327
                $path = $this->getNodeJsGlobalInstallPath();
328
            } else {
329
                $path = $this->getGlobalInstallPath($target);
330
            }
331
332
            if (strpos($path, $binDir) === 0) {
333
                // we found the local installation that already exists.
334
335
                return;
336
            }
337
        }
338
339
340
        file_put_contents($binDir.'/'.$scriptName, sprintf($content, $path));
341
        chmod($binDir.'/'.$scriptName, 0755);
342
    }
343
344
    /**
345
     * Shamelessly stolen from Symfony's FileSystem. Thanks guys!
346
     * Given an existing path, convert it to a path relative to a given starting path.
347
     *
348
     * @param string $endPath   Absolute path of target
349
     * @param string $startPath Absolute path where traversal begins
350
     *
351
     * @return string Path of target relative to starting path
352
     */
353
    private function makePathRelative($endPath, $startPath)
354
    {
355
        // Normalize separators on Windows
356
        if ('\\' === DIRECTORY_SEPARATOR) {
357
            $endPath = strtr($endPath, '\\', '/');
358
            $startPath = strtr($startPath, '\\', '/');
359
        }
360
        // Split the paths into arrays
361
        $startPathArr = explode('/', trim($startPath, '/'));
362
        $endPathArr = explode('/', trim($endPath, '/'));
363
        // Find for which directory the common path stops
364
        $index = 0;
365
        while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
366
            $index++;
367
        }
368
        // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
369
        $depth = count($startPathArr) - $index;
370
        // Repeated "../" for each level need to reach the common path
371
        $traverser = str_repeat('../', $depth);
372
        $endPathRemainder = implode('/', array_slice($endPathArr, $index));
373
        // Construct $endPath from traversing to the common path, then to the remaining $endPath
374
        $relativePath = $traverser.(strlen($endPathRemainder) > 0 ? $endPathRemainder.'/' : '');
375
376
        return (strlen($relativePath) === 0) ? './' : $relativePath;
377
    }
378
379
    private function unzip($zipFileName, $targetDir)
380
    {
381
        $zip = new \ZipArchive();
382
        $res = $zip->open($zipFileName);
383
        if ($res === true) {
384
            // extract it to the path we determined above
385
            $zip->extractTo($targetDir);
386
            $zip->close();
387
        } else {
388
            throw new NodeJsInstallerException("Unable to extract file $zipFileName");
389
        }
390
    }
391
392
    /**
393
     * Adds the vendor/bin directory into the path.
394
     * Note: the vendor/bin is prepended in order to be applied BEFORE an existing install of node.
395
     *
396
     * @param string $binDir
397
     */
398
    public function registerPath($binDir)
399
    {
400
        $path = getenv('PATH');
401
        if (Environment::isWindows()) {
402
            putenv('PATH='.realpath($binDir).';'.$path);
403
        } else {
404
            putenv('PATH='.realpath($binDir).':'.$path);
405
        }
406
    }
407
}
408