├── .gitignore ├── .npmignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── __tests__ ├── test-utils.ts └── utils │ ├── clean-test.ts │ ├── ensure-windows-test.ts │ ├── execute-child-process-test.ts │ ├── get-build-tools-installer-path-test.ts │ ├── get-is-python-installed-test.ts │ ├── get-python-installer-path-test.ts │ ├── get-work-dir-test.ts │ ├── installation-success-test.ts │ ├── logfiles │ ├── vs2015-success.txt │ └── vs2017-success.txt │ └── remove-path-test.ts ├── appveyor.yml ├── package-lock.json ├── package.json ├── ps1 ├── dry-run.ps1 ├── launch-installer.ps1 └── set-environment.ps1 ├── src ├── ambient.d.ts ├── aquire-installers.ts ├── compatible.js ├── constants.ts ├── download.ts ├── environment.ts ├── index.ts ├── install │ ├── index.ts │ ├── launch.ts │ └── tailer.ts ├── interfaces.ts ├── logging.ts ├── offline.ts ├── start.ts └── utils │ ├── clean.ts │ ├── ensure-windows.ts │ ├── execute-child-process.ts │ ├── find-logfile.ts │ ├── get-build-tools-installer-path.ts │ ├── get-build-tools-parameters.ts │ ├── get-is-python-installed.ts │ ├── get-python-installer-path.ts │ ├── get-work-dir.ts │ ├── installation-sucess.ts │ ├── remove-path.ts │ └── single-line-log.ts ├── tsconfig.json ├── tslint.json └── wallaby.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | lib 40 | dist 41 | .vscode 42 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Dependency directories 12 | node_modules 13 | jspm_packages 14 | 15 | # Optional npm cache directory 16 | .npm 17 | 18 | # Optional REPL history 19 | .node_repl_history 20 | 21 | .vscode 22 | src 23 | test 24 | appveyor.yml 25 | .gitignore 26 | .npmignore 27 | tsconfig.json 28 | tslint.json 29 | __tests__ 30 | lib 31 | 32 | # Do not publish this thing 33 | .npmrc 34 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/src/index.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": null, 13 | "runtimeExecutable": null, 14 | "runtimeArgs": [ 15 | "--nolazy" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "externalConsole": false, 21 | "sourceMaps": false, 22 | "outDir": null 23 | }, 24 | { 25 | "name": "Attach", 26 | "type": "node", 27 | "request": "attach", 28 | "port": 5858, 29 | "address": "localhost", 30 | "restart": false, 31 | "sourceMaps": false, 32 | "outDir": null, 33 | "localRoot": "${workspaceRoot}", 34 | "remoteRoot": null 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 - 2018 Felix Rieseberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Windows-Build-Tools 2 | 3 | 4 | npm version dependencies 5 | 6 | --- 7 | 8 | > Please note that the official [Node.js for Windows installer](https://nodejs.org/en/download/) can now automatically install the required tools. That's likely a much better option than the module listed here (`windows-build-tools`). 9 | 10 | --- 11 | 12 | On Windows? Want to compile [native Node modules](#examples-of-modules-supported)? Install the build tools with this one-liner. Start PowerShell as Administrator and run: 13 | 14 | ``` 15 | npm install --global windows-build-tools 16 | ``` 17 | 18 | Or, if you are using Yarn: 19 | 20 | ``` 21 | yarn global add windows-build-tools 22 | ``` 23 | 24 | ![Gif](https://user-images.githubusercontent.com/1426799/45007904-bde9f280-afb4-11e8-8a35-c77dffaffa2a.gif) 25 | 26 | After installation, npm will automatically execute this module, which downloads and installs Visual 27 | C++ Build Tools, provided free of charge for most users by Microsoft (as part of Visual Studio Community, please consult the license to determine whether or not you're eligible). These tools are [required to compile popular native modules](https://github.com/nodejs/node-gyp). 28 | If not already installed, it will also install Python 3.8, configuring your machine and npm appropriately. 29 | 30 | > :bulb: [Windows Vista / 7 only] requires [.NET Framework 4.5.1](http://www.microsoft.com/en-us/download/details.aspx?id=40773) (Currently not installed automatically by this package) 31 | 32 | Both installations are conflict-free, meaning that they do not mess with existing installations of 33 | Visual Studio, C++ Build Tools, or Python. If you see anything that indicates otherwise, please 34 | file a bug. 35 | 36 | ## Visual Studio 2017 vs Visual Studio 2015 37 | This module is capable of installing either the build tools from Visual Studio [2017](https://blogs.msdn.microsoft.com/vcblog/2016/11/16/introducing-the-visual-studio-build-tools/) or Visual 38 | Studio [2015](https://blogs.msdn.microsoft.com/vcblog/2016/03/31/announcing-the-official-release-of-the-visual-c-build-tools-2015/). 39 | 40 | By default, this tool will install the 2017 build tools. To change that, run this script with 41 | the `--vs2015` parameter. 42 | 43 | ## Usage 44 | 45 | ``` 46 | npm [--python-mirror=''] [--proxy=''] [--debug] [--strict-ssl] [--resume] [--sockets=5] [--vcc-build-tools-parameters=''] [--vs2015] [--dry-run-only] install --global windows-build-tools 47 | ``` 48 | 49 | Optional arguments: 50 | 51 | * `--offline-installers`: Path to a folder with already downloaded installers. See 52 | * `--python-mirror`: Use a given mirror to download Python (like `--python_mirror=https://npm.taobao.org/mirrors/python/`). You can alternatively set a `PYTHON_MIRROR` environment variable. 53 | * `--proxy`: Use a given proxy. You can alternatively set a `PROXY` environment variable. 54 | * `--debug`: Be extra verbose in the logger output. Equal to setting the environment variable `DEBUG` to `*`. 55 | * `--strict-ssl`: Enables "Strict SSL" mode. Defaults to false. 56 | * `--resume`: By default, `windows-build-tools` will resume aborted downloads. Set to `false` to disable. 57 | * `--sockets`: Specifies the number of http sockets to use at once (this controls concurrency). Defaults to infinity. 58 | * `--vcc-build-tools-parameters`: Specifies additional parameters for the Visual C++ Build Tools 2015. See below for more detailed usage instructions. 59 | * `--silent`: The script will not output any information. 60 | * `--vs2015`: Install the Visual Studio 2015 Build Tools instead of the Visual Studio 2017 ones. 61 | * `--dry-run-only`: Don't actually do anything, just print what the script would have done. 62 | * `--include-arm64-tools`: Include the optional Visual Studio components required to build binaries for ARM64 Windows. Only available with the 2017 and newer build tools and Node.js v12 and up. 63 | 64 | ## Supplying Parameters to the VCC Build Tools 65 | 66 | You can pass additional parameters directly to the VCC Build Tools installer. This tool does not 67 | check if the parameters make sense - passing incorrect parameters might break the whole 68 | installation. 69 | 70 | Supply parameters to `windows-build-tools` as a JSON array. Here's quick example (note the double quotes): 71 | 72 | ``` 73 | npm --vcc-build-tools-parameters='[""--allWorkloads""]' install --global windows-build-tools 74 | ``` 75 | 76 | ### Visual Studio 2015 Parameters 77 | 78 | If you run `windows-build-tools` with `--vs2015`, these parameters are available: 79 | 80 | - `/AdminFile`: Specifies the installation control file. 81 | - `/CreateAdminFile`: Specifies the location to create a control file that can then be used 82 | - `/CustomInstallPath`: Set Custom install location. 83 | - `/ForceRestart`: Always restart the system after installation. 84 | - `/Full`: Install all product features. 85 | - `/InstallSelectableItems`: Choose which selectable item(s) to be installed. 86 | -selectable item to be installed, just pass in this switch without any value. 87 | - `/Layout`: Create a copy of the media in specified folder. 88 | - `/NoRefresh`: Prevent setup checking for updates from the internet. 89 | - `/NoRestart`: Do not restart during or after installation. 90 | - `/NoWeb`: Prevent setup downloading from the internet. 91 | - `/Passive`: Display progress but do not wait for user input. 92 | - `/ProductKey`: <25-character product key> Set custom product key (no dashes). 93 | - `/PromptRestart`: Prompt the user before restarting the system. 94 | - `/Repair`: Repair the product. 95 | - `/Uninstall`: Uninstall the product. 96 | - `/Uninstall /Force`: Uninstall the product and features shared with other products. 97 | 98 | ### Visual Studio 2017 Parameters 99 | 100 | The available parameters [are documented here](https://docs.microsoft.com/en-us/visualstudio/install/use-command-line-parameters-to-install-visual-studio). 101 | 102 | ### Offline Installation 103 | 104 | By default, `windows-build-tools` will download the latest installers from Microsoft each time 105 | it's installed. Alternatively, you can prepare a folder that contains installers. They need to 106 | have their original names: 107 | 108 | * Visual Studio Build Tools: `vs_BuildTools.exe` or `BuildTools_Full.exe` 109 | * Python: `python-3.8.1.amd64.msi` or `python-3.8.1.msi` 110 | 111 | Then, run `windows-build-tools` with the `--offline-installers` argument: 112 | 113 | ```ps1 114 | npm install -g windows-build-tools --offline-installers="C:\Users\John\installers" 115 | ``` 116 | 117 | ## Support & Help 118 | 119 | This package currently only handles the most common use case, none of the edge cases. If you encounter errors, we'd greatly appreciate [error reports](https://github.com/felixrieseberg/windows-build-tools) (and even pull requests). This is currently tested on Windows 10. 120 | 121 | #### Node versions 122 | * `windows-build-tools` 4.0 and up require at least Node v8. 123 | * `windows-build-tools` 3.0 and up require at least Node v6. 124 | * `windows-build-tools` 1.0 and up require at least Node v4. 125 | 126 | 127 | #### Where is Python installed? 128 | 129 | It's saved under `%USERPROFILE%\.windows-build-tools\python38`. 130 | 131 | #### Installing as a Non-Administrator 132 | `windows-build-tools` works best if installed from an account with administrative rights. However, 133 | thanks to @brucejo75, the following steps can be taken to install to a different user account: 134 | 135 | 1. From your non-admin account (e.g. **\**) run `cmd.exe` as administrator. 136 | 2. Set the following environment variables in the new command shell: 137 | 138 | ``` 139 | set APPDATA=C:\Users\\AppData\Roaming 140 | npm config set prefix C:\Users\\AppData\Roaming\npm 141 | set USERNAME= 142 | set USERPROFILE=C:\Users\ 143 | ``` 144 | 145 | Ensure that the variables passed match your location of npm's roaming data and the location 146 | of user profiles on your machine. For ``, substitute the name of the account you want to 147 | install `windows-build-tools` for. For more information, see the `npm config set prefix` 148 | description [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions). 149 | 150 | 3. Run `npm install -g windows-build-tools` 151 | 152 | ## Examples of Modules Supported 153 | In theory, `windows-build-tools` supports all pure C++ addons for Node.js (and virtually everything 154 | else that requires a native compiler toolchain to be installed on your machine). 155 | 156 | To ensure that that's true, we take a fresh Windows 10 installation, add `windows-build-tools`, and 157 | ensure that the most popular native Node addons compile from source. Those are: [node-sass](https://www.npmjs.com/package/node-sass), [bcrypt](https://www.npmjs.com/package/bcrypt), [sqlite3](https://www.npmjs.com/package/sqlite3), [serialport](https://www.npmjs.com/package/serialport), [websocket](https://www.npmjs.com/package/websocket), [deasync](https://www.npmjs.com/package/deasync), [grpc](https://www.npmjs.com/package/grpc), [canvas](https://www.npmjs.com/package/canvas), [sharp](https://www.npmjs.com/package/sharp), 158 | [hiredis](https://www.npmjs.com/package/hiredis), [leveldown](https://www.npmjs.com/package/leveldown), [nodegit](https://www.npmjs.com/package/nodegit), [zqm](https://www.npmjs.com/package/zqm), [ffi](https://www.npmjs.com/package/ffi), [libxmljs](https://www.npmjs.com/package/libxmljs), [iconv](https://www.npmjs.com/package/iconv), [ref](https://www.npmjs.com/package/ref), [sleep](https://www.npmjs.com/package/sleep), [microtime](https://www.npmjs.com/package/microtime), [couchbase](https://www.npmjs.com/package/couchbase), [bignum](https://www.npmjs.com/package/bignum), 159 | [kerberos](https://www.npmjs.com/package/kerberos), and [ursa](https://www.npmjs.com/package/ursa). 160 | 161 | ## License & Credits 162 | 163 | The Python installation was made possible by [Ali Hajimirza](https://github.com/ali92hm), who kindly wrestled with Python's MSIs until they surrendered. For details regarding the license agreements applicable to Python, see *History and License* [3.x](https://docs.python.org/3/license.html). 164 | 165 | Use of Microsoft software is subject to the terms of the corresponding license agreements. For details regarding the license agreements applicable to Visual Studio products, refer to their [*License Directory* page](https://visualstudio.microsoft.com/license-terms/). (See also [this discussion](https://social.msdn.microsoft.com/Forums/en-US/08d62115-0b51-484f-afda-229989be9263/license-for-visual-c-2017-build-tools?forum=visualstudiogeneral) for the gist of it.) 166 | 167 | Copyright (C) 2018 Felix Rieseberg. Licensed MIT. For more details, please see LICENSE. 168 | This license applies to this package only, not to its dependencies or the 3rd party software that it installs. 169 | -------------------------------------------------------------------------------- /__tests__/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | const oldPlatform = process.platform; 4 | 5 | export function mockProcessProp(property: string, value: any) { 6 | Object.defineProperty(process, property, { 7 | value, 8 | writable: false, 9 | enumerable: true 10 | }); 11 | } 12 | 13 | export function mockPlatform(platform: string) { 14 | mockProcessProp('platform', platform); 15 | } 16 | 17 | export function resetPlatform() { 18 | mockPlatform(oldPlatform); 19 | } 20 | 21 | export class MockSpawnChild extends EventEmitter { 22 | public stdin = { 23 | end: jest.fn() 24 | }; 25 | } 26 | 27 | export const mockSpawnChild = new MockSpawnChild(); 28 | 29 | export const mockSpawnOk = () => { 30 | setTimeout(() => mockSpawnChild.emit('exit', 0), 200); 31 | return mockSpawnChild; 32 | }; 33 | 34 | export const mockSpawnError = () => { 35 | setTimeout(() => mockSpawnChild.emit('exit', 1), 200); 36 | return mockSpawnChild; 37 | }; 38 | -------------------------------------------------------------------------------- /__tests__/utils/clean-test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { cleanExistingLogFiles } from '../../src/utils/clean'; 3 | 4 | const mockBuildToolsInstallerPath = 'C:\\Users\\test\\.windows-build=tools\buildTools.exe'; 5 | const mockPythonInstallerPath = 'C:\\Users\\test\\.windows-build=tools\python.exe'; 6 | 7 | jest.mock('fs', () => ({ 8 | existsSync: jest.fn(), 9 | unlinkSync: jest.fn() 10 | })); 11 | 12 | jest.mock('../../src/utils/get-build-tools-installer-path', () => ({ 13 | getBuildToolsInstallerPath: jest.fn(() => ({ 14 | logPath: mockBuildToolsInstallerPath 15 | })) 16 | })); 17 | 18 | jest.mock('../../src/utils/get-python-installer-path', () => ({ 19 | getPythonInstallerPath: jest.fn(() => ({ 20 | logPath: mockPythonInstallerPath 21 | })) 22 | })); 23 | 24 | describe('clean', () => { 25 | it('does not attempt to delete non-existing files', () => { 26 | cleanExistingLogFiles(); 27 | 28 | expect(fs.existsSync).toHaveBeenCalledTimes(2); 29 | expect(fs.unlinkSync).toHaveBeenCalledTimes(0); 30 | }); 31 | 32 | it('does attempt to delete existing files', () => { 33 | (fs.existsSync as any).mockReturnValue(true); 34 | 35 | cleanExistingLogFiles(); 36 | 37 | expect(fs.existsSync).toHaveBeenCalledTimes(4); 38 | expect(fs.unlinkSync).toHaveBeenCalledTimes(2); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /__tests__/utils/ensure-windows-test.ts: -------------------------------------------------------------------------------- 1 | import { ensureWindows } from '../../src/utils/ensure-windows'; 2 | import { mockPlatform, mockProcessProp, resetPlatform } from '../test-utils'; 3 | 4 | describe('ensure-windows', () => { 5 | const oldExit = process.exit; 6 | 7 | beforeEach(() => { 8 | mockProcessProp('exit', jest.fn()); 9 | }); 10 | 11 | afterEach(() => { 12 | resetPlatform(); 13 | mockProcessProp('exit', oldExit); 14 | }); 15 | 16 | it('exits on macOS', () => { 17 | mockPlatform('darwin'); 18 | 19 | ensureWindows(); 20 | 21 | expect(process.exit).toHaveBeenCalled(); 22 | }); 23 | 24 | it('exits on Linux', () => { 25 | mockPlatform('linux'); 26 | 27 | ensureWindows(); 28 | 29 | expect(process.exit).toHaveBeenCalled(); 30 | }); 31 | 32 | it('does not exit on Windows', () => { 33 | mockPlatform('win32'); 34 | 35 | ensureWindows(); 36 | 37 | expect(process.exit).toHaveBeenCalledTimes(0); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /__tests__/utils/execute-child-process-test.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import { executeChildProcess } from '../../src/utils/execute-child-process'; 3 | import { mockSpawnChild, mockSpawnError, mockSpawnOk } from '../test-utils'; 4 | 5 | jest.mock('child_process', () => ({ spawn: jest.fn()})); 6 | 7 | describe('execute-child-process', () => { 8 | it('resolves the promise if the child exits with code 0', async () => { 9 | (spawn as any).mockImplementation(mockSpawnOk); 10 | 11 | await executeChildProcess('fake', []); 12 | }); 13 | 14 | it('rejects the promise if the child exits with code 1', async () => { 15 | (spawn as any).mockImplementation(mockSpawnError); 16 | 17 | const expectedError = new Error('fake exited with code: 1'); 18 | 19 | return expect(executeChildProcess('fake', [])).rejects.toEqual(expectedError); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/utils/get-build-tools-installer-path-test.ts: -------------------------------------------------------------------------------- 1 | jest.mock('../../src/utils/get-work-dir', () => ({ 2 | getWorkDirectory: jest.fn(() => 'C:\\workDir') 3 | })); 4 | 5 | jest.mock('../../src/utils/get-is-python-installed', () => ({ 6 | getIsPythonInstalled: jest.fn(() => null) 7 | })); 8 | 9 | describe('getBuildToolsInstallerPath', () => { 10 | it('gets the correct information (2015)', () => { 11 | process.env.npm_config_vs2015 = 'true'; 12 | 13 | const { getBuildToolsInstallerPath } = require('../../src/utils/get-build-tools-installer-path'); 14 | 15 | expect(getBuildToolsInstallerPath()).toEqual({ 16 | directory: 'C:\\workDir', 17 | fileName: 'BuildTools_Full.exe', 18 | logPath: 'C:\\workDir\\build-tools-log.txt', 19 | path: 'C:\\workDir\\BuildTools_Full.exe', 20 | url: 'https://download.microsoft.com/download/5/f/7/5f7acaeb-8363-451f-9425-68a90f98b238/visualcppbuildtools_full.exe', 21 | }); 22 | 23 | delete process.env.npm_config_vs2015; 24 | }); 25 | 26 | it('gets the correct information (2017)', () => { 27 | process.env.npm_config_vs2017 = 'true'; 28 | 29 | jest.resetModules(); 30 | 31 | const { getBuildToolsInstallerPath } = require('../../src/utils/get-build-tools-installer-path'); 32 | 33 | expect(getBuildToolsInstallerPath()).toEqual({ 34 | directory: 'C:\\workDir', 35 | fileName: 'vs_BuildTools.exe', 36 | logPath: null, 37 | path: 'C:\\workDir\\vs_BuildTools.exe', 38 | url: 'https://download.visualstudio.microsoft.com/download/pr/11503713/e64d79b40219aea618ce2fe10ebd5f0d/vs_BuildTools.exe', 39 | }); 40 | 41 | delete process.env.npm_config_vs2017; 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /__tests__/utils/get-is-python-installed-test.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process'; 2 | 3 | jest.mock('child_process', () => ({ spawnSync: jest.fn()})); 4 | 5 | describe('get-is-python-installed', () => { 6 | let getIsPythonInstalled: any; 7 | 8 | beforeEach(() => { 9 | ({ getIsPythonInstalled } = require('../../src/utils/get-is-python-installed')); 10 | jest.resetModules(); 11 | }); 12 | 13 | it('correctly returns the Python version if installed', async () => { 14 | (spawnSync as any).mockReturnValue({ output: '\nPython 3.8.1\n' }); 15 | 16 | expect(getIsPythonInstalled()).toBe('Python 3.8.1'); 17 | }); 18 | 19 | it('correctly returns the Python version if installed', async () => { 20 | (spawnSync as any).mockImplementation(() => { 21 | throw new Error('No!'); 22 | }); 23 | 24 | expect(getIsPythonInstalled()).toBeNull(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/utils/get-python-installer-path-test.ts: -------------------------------------------------------------------------------- 1 | import { getPythonInstallerPath } from '../../src/utils/get-python-installer-path'; 2 | 3 | jest.mock('../../src/utils/get-work-dir', () => ({ 4 | getWorkDirectory: jest.fn(() => 'C:\\workDir') 5 | })); 6 | 7 | jest.mock('../../src/utils/get-is-python-installed', () => ({ 8 | getIsPythonInstalled: jest.fn(() => null) 9 | })); 10 | 11 | describe('getPythonInstallerPath', () => { 12 | it('gets the correct information', () => { 13 | const amd64 = process.arch === 'x64' ? 'amd64.' : ''; 14 | 15 | expect(getPythonInstallerPath()).toEqual({ 16 | directory: 'C:\\workDir', 17 | fileName: `python-3.8.1.${amd64}msi`, 18 | logPath: 'C:\\workDir\\python-log.txt', 19 | path: `C:\\workDir\\python-3.8.1.${amd64}msi`, 20 | targetPath: 'C:\\workDir\\python38', 21 | url: `https://www.python.org/ftp/python/3.8.1/python-3.8.1.${amd64}msi`, 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/utils/get-work-dir-test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | import { getWorkDirectory } from '../../src/utils/get-work-dir'; 3 | 4 | jest.mock('fs-extra', () => ({ 5 | ensureDirSync: jest.fn() 6 | })); 7 | 8 | describe('get-work-dir', () => { 9 | it('returns a working directory and ensures it exists', () => { 10 | expect(getWorkDirectory()).toBeDefined(); 11 | expect(fs.ensureDirSync).toHaveBeenCalledTimes(1); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/utils/installation-success-test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | 3 | jest.mock('../../src/constants', () => ({ 4 | BUILD_TOOLS: { version: 2015 } 5 | })); 6 | 7 | describe('installation-success', () => { 8 | describe('includesSuccess', () => { 9 | const { includesSuccess } = require('../../src/utils/installation-sucess'); 10 | 11 | it('correctly reports success', () => { 12 | expect(includesSuccess('Variable: IsInstalled = 1')).toEqual({ 13 | isBuildToolsSuccess: true, 14 | isPythonSuccess: false 15 | }); 16 | 17 | expect(includesSuccess('Variable: BuildTools_Core_Installed = ')).toEqual({ 18 | isBuildToolsSuccess: true, 19 | isPythonSuccess: false 20 | }); 21 | 22 | expect(includesSuccess('WixBundleInstalled = 1')).toEqual({ 23 | isBuildToolsSuccess: true, 24 | isPythonSuccess: false 25 | }); 26 | 27 | expect(includesSuccess('Blubblub')).toEqual({ 28 | isBuildToolsSuccess: false, 29 | isPythonSuccess: false 30 | }); 31 | 32 | expect(includesSuccess('INSTALL. Return value 1')).toEqual({ 33 | isBuildToolsSuccess: false, 34 | isPythonSuccess: true 35 | }); 36 | 37 | expect(includesSuccess('Installation completed successfully')).toEqual({ 38 | isBuildToolsSuccess: false, 39 | isPythonSuccess: true 40 | }); 41 | 42 | expect(includesSuccess('Configuration completed successfully')).toEqual({ 43 | isBuildToolsSuccess: false, 44 | isPythonSuccess: true 45 | }); 46 | }); 47 | }); 48 | 49 | describe('includesFailure', () => { 50 | const { includesFailure } = require('../../src/utils/installation-sucess'); 51 | 52 | it('correctly reports failure for build tools', () => { 53 | expect(includesFailure('Closing installer. Return code: -13')).toEqual({ 54 | isBuildToolsFailure: true, 55 | isPythonFailure: false 56 | }); 57 | 58 | expect(includesFailure('Shutting down, exit code: -13')).toEqual({ 59 | isBuildToolsFailure: true, 60 | isPythonFailure: false 61 | }); 62 | }); 63 | 64 | it('correctly reports failure for Python', () => { 65 | expect(includesFailure('(64-bit) -- Installation failed.')).toEqual({ 66 | isBuildToolsFailure: false, 67 | isPythonFailure: true 68 | }); 69 | }); 70 | }); 71 | 72 | describe('VS log files', () => { 73 | function testLog(file, installEndLine, success, vsVersion) { 74 | // file must end in .txt because .log is gitignored 75 | // installEndLine is the first line (zero based) of the last block of timestamps, 76 | // it should point to the moment the installer starts cleaning up. 77 | 78 | jest.setMock('../../src/constants', { 79 | BUILD_TOOLS: { version: vsVersion } 80 | }); 81 | jest.resetModules(); 82 | const { includesSuccess, includesFailure } = require('../../src/utils/installation-sucess'); 83 | 84 | const finalText = fs.readFileSync(`${__dirname}/logfiles/${file}.txt`, 'utf8'); 85 | const installingText = finalText.split(/\r?\n/).slice(0, installEndLine).join('\n'); 86 | 87 | expect(includesSuccess(installingText).isBuildToolsSuccess).toEqual(false); 88 | expect(includesFailure(installingText).isBuildToolsFailure).toEqual(false); 89 | 90 | if (success) { 91 | expect(includesSuccess(finalText).isBuildToolsSuccess).toEqual(true); 92 | // Don't check failure, it can be true in case of success. 93 | } else { 94 | expect(includesSuccess(finalText).isBuildToolsSuccess).toEqual(false); 95 | expect(includesFailure(finalText).isBuildToolsFailure).toEqual(true); 96 | } 97 | } 98 | 99 | it('VS2015 successful intallation', () => { 100 | testLog('vs2015-success', 3494, true, 2015); 101 | }); 102 | 103 | it('VS2017 successful intallation', () => { 104 | testLog('vs2017-success', 75, true, 2017); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /__tests__/utils/logfiles/vs2017-success.txt: -------------------------------------------------------------------------------- 1 | 2018-09-17T18:45:27 : Verbose : Visual Studio Installer (1.17.1298.831 : release) ["C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vs_installershell.exe","/finalizeInstall","install","--in","C:\\ProgramData\\Microsoft\\VisualStudio\\Packages\\_bootstrapper\\vs_setup_bootstrapper_201809171844423537.json","--norestart","--quiet","--includeRecommended","--add","Microsoft.VisualStudio.Workload.VCTools","--locale","en-US","--activityId","8ab09e10-1c21-4f3b-878a-a7fbc48c966b"] 2 | 2018-09-17T18:45:30 : Verbose : Received the application ready notification 3 | 2018-09-17T18:45:30 : Verbose : ProgressBarService listening to ipc channel: progress-bar 4 | 2018-09-17T18:45:30 : Verbose : LoggerIpcRpcService listening to ipc channel: LoggerService 5 | 2018-09-17T18:45:30 : Verbose : LoggerIpcRpcService listening to ipc channel: LoggerService 6 | 2018-09-17T18:45:30 : Verbose : Telemetry Session ID: cafa6b28-db3d-46d0-96ed-6c8dc1acc955 7 | 2018-09-17T18:45:30 : Verbose : Starting ServiceHub Remote Settings client. 8 | 2018-09-17T18:45:30 : Verbose : Creating commonError Service 9 | 2018-09-17T18:45:30 : Verbose : starting rpc process for common error 10 | 2018-09-17T18:45:30 : Verbose : RPC Factory: Get common error config provider 11 | 2018-09-17T18:45:30 : Verbose : FeaturesIpcRpcService listening to ipc channel: FeaturesProxy 12 | 2018-09-17T18:45:30 : Verbose : FeaturesIpcRpcService listening to ipc channel: FeaturesProxy 13 | 2018-09-17T18:45:30 : Verbose : Creating Start menu shortcut [shortcutName: Visual Studio Installer, targetPath: C:\Program Files (x86)\Microsoft Visual Studio\Installer\vs_installer.exe] 14 | 2018-09-17T18:45:30 : Verbose : Starting the setup updater service. 15 | 2018-09-17T18:45:30 : Verbose : Service creation finished 16 | 2018-09-17T18:45:30 : Verbose : Installation finalized successfully. 17 | 2018-09-17T18:45:31 : Verbose : FeedbackIpcRpcService listening to ipc channel: FeedbackProxy 18 | 2018-09-17T18:45:31 : Verbose : Window ready 19 | 2018-09-17T18:45:31 : Verbose : Getting installed product summaries. [installerId: SetupEngine] 20 | 2018-09-17T18:45:31 : Verbose : Starting the installed products provider service. 21 | 2018-09-17T18:45:31 : Verbose : Starting the products provider service. 22 | 2018-09-17T18:45:31 : Verbose : Getting product summaries. [installerId: SetupEngine] 23 | 2018-09-17T18:45:31 : Verbose : Starting the installer service. 24 | 2018-09-17T18:45:32 : Verbose : Connected to Hub Controller's client watch 'net.pipe://106ff59fa7d9c41fd2232572c91a1d6a' 25 | 2018-09-17T18:45:32 : Verbose : Calling SetupEngine.Installer.Initialize. [locale: en-US] 26 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.Initialize(initializer) initializer: {"ClientName":"vs-xsetup","ClientVersion":"1.17.1298.831","RemoteSettingsFileName":"RemoteSettings_Installer.json","SerializedTelemetrySession":"{\"IsOptedIn\":true,\"Id\":\"cafa6b28-db3d-46d0-96ed-6c8dc1acc955\",\"HostName\":\"Dev14\",\"AppInsightsInstrumentationKey\":\"f144292e-e3b2-4011-ac90-20e5c03fbce5\",\"AsimovInstrumentationKey\":\"aif-312cbd79-9dbb-4c48-a7da-3cc2a931cb70\",\"ProcessStartTime\":636728319298690000}","ChannelOrProductId":"VS Installer","AppIdGuid":"42123B45-5471-4B16-81E7-5404CD93BCF1","ClientLocale":"en-US","Flights":[]}} 27 | 2018-09-17T18:45:32 : Verbose : ServiceHub Remote Settings client started. 28 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: ProblemsDlgButtons, defaultValue: false 29 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: CommonError, defaultValue: false 30 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: ProblemsDlgRetry, defaultValue: false 31 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: InstallationOptionsPageKS, defaultValue: false 32 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: CloudNativeDesc, defaultValue: false 33 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: CloudFirstDesc, defaultValue: false 34 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: RecWklds, defaultValue: false 35 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: SortWklds, defaultValue: false 36 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: ShowBitrate, defaultValue: false 37 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: RecommendSel, defaultValue: false 38 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: Surveys, defaultValue: false 39 | 2018-09-17T18:45:32 : Verbose : Calling RemoteSettingsProviderService.GetActionsAsync(actionPath), actionPath: vs\installer\commonerroractions 40 | 2018-09-17T18:45:33 : Verbose : SetupEngine.Installer.Initialize succeeded. [locale: en-US] 41 | 2018-09-17T18:45:33 : Verbose : Started the installer service. 42 | 2018-09-17T18:45:33 : Verbose : Calling SetupEngine.Installer.GetDriveInfo. 43 | 2018-09-17T18:45:33 : Verbose : Calling SetupEngine.Installer.IsElevated. 44 | 2018-09-17T18:45:33 : Verbose : Started the setup updater service. 45 | 2018-09-17T18:45:33 : Verbose : ServiceHubExperimentationClient.setSharedProperty(name, value) called, 46 | [name: VS.ABExp.Flights] [value: ] 47 | 2018-09-17T18:45:33 : Verbose : SetupEngine.Installer.GetDriveInfo succeeded. 48 | 2018-09-17T18:45:33 : Verbose : SetupEngine.Installer.IsElevated succeeded. 49 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: CommonError, defaultValue: false, result: false 50 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: ProblemsDlgButtons, defaultValue: false, result: false 51 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: ProblemsDlgRetry, defaultValue: false, result: false 52 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: InstallationOptionsPageKS, defaultValue: false, result: false 53 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: CloudNativeDesc, defaultValue: false, result: false 54 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: RecWklds, defaultValue: false, result: false 55 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: CloudFirstDesc, defaultValue: false, result: false 56 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: ShowBitrate, defaultValue: false, result: false 57 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: SortWklds, defaultValue: false, result: false 58 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: Surveys, defaultValue: false, result: false 59 | 2018-09-17T18:45:33 : Verbose : Resolved RemoteSettingsProviderService.GetBooleanValue(collectionPath, key, defaultValue) collectionPath: Installer\Features\, key: RecommendSel, defaultValue: false, result: false 60 | 2018-09-17T18:45:33 : Verbose : ServiceHubExperimentationClient.setSharedProperty(name, value) called, 61 | [name: VS.ABExp.Flights] [value: lazytoolboxinit;fwlargebuffer;refactoring;spmoretempsbtn1;asloff;keybindgoldbarext] 62 | 2018-09-17T18:45:33 : Verbose : ServiceHubExperimentationClient.setSharedProperty(name, value) called, 63 | [name: VS.ABExp.Flights] [value: lazytoolboxinit;fwlargebuffer;refactoring;spmoretempsbtn1;asloff;keybindgoldbarext;tn-rsv-14u3;tn-nps-15b;tn-vsmacnps-7] 64 | 2018-09-17T18:45:34 : Verbose : Resolved RemoteSettingsProviderService.GetActionsAsync(actionPath) actionPath: vs\installer\commonerroractions, result: [{"Action":{"Rules":[{"ErrorCode":"0x80072ee7","CommonErrorMessage":"**Error 0x80072ee7: No internet connection**\r\nThere’s a problem connecting to our content delivery network servers. This might be a temporary server outage, or it could be an issue with your internet connection.\r\n\r\n**Fix**: First, select View Logs and try to go to the URL in the report. If you can, try installing Visual Studio again. If you can’t access the URL in the View Logs report, or if you are on an unreliable network, please try our recommended steps to [install Visual Studio 2017 on low–bandwidth or unreliable network environments](https://docs.microsoft.com/en-us/visualstudio/install/install-vs-inconsistent-quality-network).","Resources":[{"Locale":"en-us","Message":"**Error 0x80072ee7: No internet connection**\r\nThere’s a problem connecting to our content delivery network servers. This might be a temporary server outage, or it could be an issue with your internet connection.\r\n\r\n**Fix**: First, select View Logs and try to go to the URL in the report. If you can, try installing Visual Studio again. If you can’t access the URL in the View Logs report, or if you are on an unreliable network, please try our recommended steps to [install Visual Studio 2017 on low–bandwidth or unreliable network environments](https://docs.microsoft.com/en-us/visualstudio/install/install-vs-inconsistent-quality-network)."}]}]},"Precedence":0,"RuleId":"1343144E-3B75-4673-AA34-82674D5F4AB0"},{"Action":{"Rules":[{"ErrorCode":"-2146233033","CommonErrorMessage":"**Error –2146233033: Package is corrupted**\r\nThe Visual Studio installer found a corrupted package. A package usually gets partially saved if the system was interrupted while downloading and writing the package to the disk.\r\n\r\n**Fix**: This issue usually resolves by running a repair.","Resources":[{"Locale":"en-us","Message":"**Error –2146233033: Package is corrupted**\r\nThe Visual Studio installer found a corrupted package. A package usually gets partially saved if the system was interrupted while downloading and writing the package to the disk.\r\n\r\n**Fix**: This issue usually resolves by running a repair."}]}]},"Precedence":0,"RuleId":"17674D21-417D-45EB-B671-58D521DF3D57"},{"Action":{"Rules":[{"ErrorCode":"1303","CommonErrorMessage":"**Error 1303: Access is denied**\r\nThere’s a problem accessing a file or folder. This can happen if another process has locked a folder. \n \r\n\r\n**FIX**: Rebooting and running a repair has helped unblock other users.","Resources":[{"Locale":"en-us","Message":"**Error 1303: Access is denied**\r\nThere’s a problem accessing a file or folder. This can happen if another process has locked a folder. \n \r\n\r\n**FIX**: Rebooting and running a repair has helped unblock other users."}]}]},"Precedence":0,"RuleId":"384E5F4B-26EC-4277-94B4-74E2A843FBA9"},{"Action":{"Rules":[{"ErrorCode":"1714","CommonErrorMessage":"**Error Code 1714: Cannot uninstall older version of an MSI**\r\nAn older version of an MSI is on your computer. You’ll need to uninstall it before you can install Visual Studio.\r\n\r\n**Fix**: Please try the recommended steps to [troubleshoot MSI errors 1714 and 1612](https://aka.ms/VSMSIError)","Resources":[{"Locale":"en-us","Message":"**Error Code 1714: Cannot uninstall older version of an MSI**\r\nAn older version of an MSI is on your computer. You’ll need to uninstall it before you can install Visual Studio.\r\n\r\n**Fix**: Please try the recommended steps to [troubleshoot MSI errors 1714 and 1612](https://aka.ms/VSMSIError)"}]}]},"Precedence":0,"RuleId":"818EBFDE-E794-4A31-AA70-2C427B920C33"},{"Action":{"Rules":[{"ErrorCode":"0x80096004","CommonErrorMessage":"**Error 0x80096004: Signature can't be verified**\r\nThere’s a problem verifying the signature of a certificate. This usually happens when a package has been damaged.\r\n\r\n**FIX**: First, select View Logs to get the name of the damaged package. In File Explorer, delete that package from the %ProgramData%\\Microsoft\\VisualStudio\\Packages folder and try installing Visual Studio again. If you’re installing from a local or network layout, try following the recommended steps to [update a network–based installation of Visual Studio.](https://docs.microsoft.com/en-us/visualstudio/install/update-a-network-installation-of-visual-studio)","Resources":[{"Locale":"en-us","Message":"**Error 0x80096004: Signature can't be verified**\r\nThere’s a problem verifying the signature of a certificate. This usually happens when a package has been damaged.\r\n\r\n**FIX**: First, select View Logs to get the name of the damaged package. In File Explorer, delete that package from the %ProgramData%\\Microsoft\\VisualStudio\\Packages folder and try installing Visual Studio again. If you’re installing from a local or network layout, try following the recommended steps to [update a network–based installation of Visual Studio.](https://docs.microsoft.com/en-us/visualstudio/install/update-a-network-installation-of-visual-studio)"}]}]},"Precedence":0,"RuleId":"8A7E5F65-4C5B-43ED-8360-6542C6FC958C"},{"Action":{"Rules":[{"ErrorCode":"1618","CommonErrorMessage":"**Error 1618: Another installation in progress**\r\nAnother program is using the Windows Installer Service. This might be due to an automatic update.\r\n\r\n**Fix**: If Windows is installing updates, we recommend that you wait until the update is complete. If an installation didn’t close properly, select the Processes tab in Task Manager to locate and stop any MSIExec.exe entries. You can also restart your computer and try installing again.","Resources":[{"Locale":"en-us","Message":"**Error 1618: Another installation in progress**\r\nAnother program is using the Windows Installer Service. This might be due to an automatic update.\r\n\r\n**Fix**: If Windows is installing updates, we recommend that you wait until the update is complete. If an installation didn’t close properly, select the Processes tab in Task Manager to locate and stop any MSIExec.exe entries. You can also restart your computer and try installing again."}]}]},"Precedence":0,"RuleId":"B0F3C77A-61DC-4C6E-945B-60B6519BC988"},{"Action":{"Rules":[{"ErrorCode":"1601","CommonErrorMessage":"**Error 1601: Windows Installer Service unavailable**\r\nThere’s a problem with the Windows Installer Service, or it’s been turned off.\r\n\r\n\n**Fix**: This problem usually resolves when you retry the Visual Studio installation after one of the following workarounds.\r\n1. Restart your computer. 2. Make sure Windows Installer Service is turned on. Run Services.msc and double-click on Windows Installer. If it’s turned off, change Status to Manual, then select Apply and OK. If it’s already running, select Stop and then Start. 3. If you’re still getting this problem, try the [recommended Windows Installer service troubleshooting steps](https://support.microsoft.com/en-us/help/17588/fix-problems-that-block-programs-from-being-installed-or-removed).","Resources":[{"Locale":"en-us","Message":"**Error 1601: Windows Installer Service unavailable**\r\nThere’s a problem with the Windows Installer Service, or it’s been turned off.\r\n\r\n\n**Fix**: This problem usually resolves when you retry the Visual Studio installation after one of the following workarounds.\r\n1. Restart your computer.\r\n2. Make sure Windows Installer Service is turned on. Run Services.msc and double-click on Windows Installer. If it’s turned off, change Status to Manual, then select Apply and OK. If it’s already running, select Stop and then Start.\r\n3. If you’re still getting this problem, try the [recommended Windows Installer service troubleshooting steps](https://support.microsoft.com/en-us/help/17588/fix-problems-that-block-programs-from-being-installed-or-removed))"}]}]},"Precedence":0,"RuleId":"DFC5CD1E-D038-4A37-B5DD-82A4D220E0B9"}] 65 | 2018-09-17T18:45:35 : Verbose : Started the products provider service. 66 | 2018-09-17T18:45:35 : Verbose : Dispatched ProductUpdateCheckStartedEvent event. 67 | 2018-09-17T18:45:35 : Verbose : Started the installed products provider service. 68 | 2018-09-17T18:45:35 : Verbose : Channel: VisualStudio.15.Release, status: Cached 69 | 2018-09-17T18:45:35 : Verbose : Getting product. [installerId: SetupEngine, productId: Microsoft.VisualStudio.Product.BuildTools]. 70 | 2018-09-17T18:45:36 : Verbose : Dispatched ProductUpdateCheckFinishedEvent event with wasManifestUpdated=false. 71 | 2018-09-17T18:45:38 : Verbose : Calling SetupEngine.Installer.EvaluateInstallParameters. [channelId: VisualStudio.15.Release, productId: Microsoft.VisualStudio.Product.BuildTools, installationPath: 'C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools', languages: 'en-US' selectedPackageReferences.length: 13] 72 | 2018-09-17T18:45:38 : Verbose : SetupEngine.Installer.EvaluateInstallParameters succeeded. [channelId: VisualStudio.15.Release, productId: Microsoft.VisualStudio.Product.BuildTools, installationPath: 'C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools', languages: 'en-US' selectedPackageReferences.length: 13] 73 | 2018-09-17T18:45:38 : Verbose : Calling SetupEngine.Installer.EvaluateInstallParameters. [channelId: VisualStudio.15.Release, productId: Microsoft.VisualStudio.Product.BuildTools, installationPath: 'C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools', languages: 'en-US' selectedPackageReferences.length: 13] 74 | 2018-09-17T18:45:38 : Verbose : SetupEngine.Installer.EvaluateInstallParameters succeeded. [channelId: VisualStudio.15.Release, productId: Microsoft.VisualStudio.Product.BuildTools, installationPath: 'C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools', languages: 'en-US' selectedPackageReferences.length: 13] 75 | 2018-09-17T18:45:38 : Verbose : Calling SetupEngine.Installer.InstallProduct. [channelId: VisualStudio.15.Release, productId: Microsoft.VisualStudio.Product.BuildTools, installationPath: 'C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools'] 76 | 2018-09-17T18:53:17 : Verbose : SetupEngine.Installer.InstallProduct succeeded. [channelId: VisualStudio.15.Release, productId: Microsoft.VisualStudio.Product.BuildTools, installationPath: 'C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools'] 77 | 2018-09-17T18:53:17 : Verbose : Closing installer. Return code: 0. 78 | 2018-09-17T18:53:17 : Verbose : [ProductsProviderImpl]: Rpc connection was closed. 79 | 2018-09-17T18:53:17 : Verbose : [ProductsProviderImpl]: Stream was closed 80 | 2018-09-17T18:53:17 : Verbose : [InstalledProductsProviderImpl]: Rpc connection was closed. 81 | 2018-09-17T18:53:17 : Verbose : [InstalledProductsProviderImpl]: Stream was closed 82 | 2018-09-17T18:53:17 : Verbose : [InstallerImpl]: Rpc connection was closed. 83 | 2018-09-17T18:53:17 : Verbose : [InstallerImpl]: Stream was closed 84 | 2018-09-17T18:53:17 : Verbose : [SetupUpdaterImpl]: Rpc connection was closed. 85 | 2018-09-17T18:53:17 : Verbose : [SetupUpdaterImpl]: Stream was closed 86 | -------------------------------------------------------------------------------- /__tests__/utils/remove-path-test.ts: -------------------------------------------------------------------------------- 1 | import { removePath } from '../../src/utils/remove-path'; 2 | 3 | describe('removePath', () => { 4 | it('removes the path', () => { 5 | process.env.PATH = process.env.path = 'hi'; 6 | 7 | removePath(); 8 | 9 | // process.env.path is weird and we 10 | // need a weird test for it 11 | Object.keys(process.env).forEach((k) => { 12 | expect(k !== 'PATH').toBe(true); 13 | expect(k !== 'path').toBe(true); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | 4 | # Test against these versions of Node.js. 5 | environment: 6 | matrix: 7 | - nodejs_version: "10.0" 8 | 9 | # Install scripts. (runs after repo cloning) 10 | install: 11 | - ps: | 12 | if ($env:NPM_TOKEN.length -eq 0) { 13 | Write-Host "This job originated from an untrusted source."; 14 | if (Test-Path ./.npmrc) { 15 | Write-Host "Renaming .npmrc to disable it"; 16 | Rename-Item -Path ./.npmrc -NewName "npmrcdisabled"; 17 | } 18 | } 19 | - ps: Install-Product node $env:nodejs_version 20 | - npm install --ignore-scripts 21 | 22 | # Post-install test scripts. 23 | test_script: 24 | # Output useful info for debugging. 25 | - node --version 26 | - npm --version 27 | # run tests 28 | - npm test 29 | 30 | build: off 31 | 32 | deploy_script: 33 | - if %APPVEYOR_REPO_TAG% EQU true npm config set '//registry.npmjs.org/:_authToken' "%NPM_TOKEN%" && npm publish . 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "windows-build-tools", 3 | "version": "5.3.0", 4 | "description": "Install C++ Build Tools for Windows using npm", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "npm run lint && npm run unit", 8 | "unit": "jest", 9 | "postinstall": "node ./dist/index.js", 10 | "build": "tsc -p tsconfig.json", 11 | "prepare": "npm run build", 12 | "start": "npm run build && npm run postinstall", 13 | "lint": "tslint -c tslint.json -p tsconfig.json \"src/**/*.ts\"" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/felixrieseberg/windows-build-tools.git" 18 | }, 19 | "os": [ 20 | "win32" 21 | ], 22 | "keywords": [ 23 | "Windows", 24 | "Build Tools", 25 | "node-gyp", 26 | "native", 27 | "c++" 28 | ], 29 | "engines": { 30 | "node": ">=8.0.0" 31 | }, 32 | "author": "Felix Rieseberg ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/felixrieseberg/windows-build-tools/issues" 36 | }, 37 | "homepage": "https://github.com/felixrieseberg/windows-build-tools#readme", 38 | "devDependencies": { 39 | "@types/chalk": "^2.2.0", 40 | "@types/fs-extra": "^8.1.1", 41 | "@types/jest": "^24.0.13", 42 | "@types/node": "^12.0.8", 43 | "jest": "^24.8.0", 44 | "ts-jest": "^24.0.2", 45 | "tslint": "^6.1.3", 46 | "tslint-microsoft-contrib": "^6.2.0", 47 | "typescript": "^4.3.4" 48 | }, 49 | "dependencies": { 50 | "chalk": "^2.4.2", 51 | "debug": "^4.3.1", 52 | "fs-extra": "^8.1.0", 53 | "in-gfw": "^1.2.0", 54 | "nugget": "^2.0.1", 55 | "string-width": "^4.2.2" 56 | }, 57 | "jest": { 58 | "moduleFileExtensions": [ 59 | "ts", 60 | "tsx", 61 | "js" 62 | ], 63 | "transform": { 64 | "^.+\\.(ts|tsx)$": "ts-jest" 65 | }, 66 | "globals": { 67 | "ts-jest": { 68 | "tsConfigFile": "tsconfig.json" 69 | } 70 | }, 71 | "testMatch": [ 72 | "**/__tests__/**/*-test.+(ts|tsx|js)" 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ps1/dry-run.ps1: -------------------------------------------------------------------------------- 1 | # Dry-run, do nothing 2 | Write-Output "Passed arguments: $args" 3 | -------------------------------------------------------------------------------- /ps1/launch-installer.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [string]$BuildToolsInstallerPath, 4 | [string]$ExtraBuildToolsParameters, 5 | [string]$PythonInstaller, 6 | [string]$VisualStudioVersion, 7 | [switch]$InstallPython, 8 | [switch]$InstallBuildTools 9 | ) 10 | 11 | # Returns whether or not the current user has administrative privileges 12 | function IsAdministrator { 13 | $Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() 14 | $Principal = New-Object System.Security.Principal.WindowsPrincipal($Identity) 15 | $Principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) 16 | } 17 | 18 | # Returns whether or not UAC is enabled on Windows 19 | function IsUacEnabled { 20 | (Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System).EnableLua -ne 0 21 | } 22 | 23 | # Runs the installer 24 | function runInstaller { 25 | if (Test-Path $BuildToolsInstallerPath) { 26 | $extraParams = $ExtraBuildToolsParameters -split "%_; " 27 | 28 | if ($extraParams.count -gt 0) { 29 | foreach ($element in $extraParams) { 30 | $params += $element 31 | } 32 | } 33 | 34 | cd $BuildToolsInstallerPath 35 | 36 | if ($VisualStudioVersion -eq "2017") { 37 | $params = "--norestart", "--quiet", "--includeRecommended", "--add", "Microsoft.VisualStudio.Workload.VCTools" 38 | ./vs_BuildTools.exe $params 39 | } else { 40 | $params = "/NoRestart", "/S", "/L", "`"$BuildToolsInstallerPath\build-tools-log.txt`"" 41 | ./BuildTools_Full.exe $params 42 | } 43 | } else { 44 | Write-Output "Tried to start Build Tools installer, but couldn't find $BuildToolsInstallerPath." 45 | } 46 | } 47 | 48 | function runPythonInstaller { 49 | if (Test-Path $BuildToolsInstallerPath) { 50 | cd $BuildToolsInstallerPath 51 | $pyParams = "/i", $PythonInstaller, "TARGETDIR=```"$BuildToolsInstallerPath\python38```"", "ALLUSERS=0", "/qn", "/L*P", "`"$BuildToolsInstallerPath\python-log.txt`"" 52 | Invoke-Expression "msiexec.exe $pyParams" 53 | } else { 54 | Write-Output "Tried to start Python installer, but couldn't find $BuildToolsInstallerPath." 55 | } 56 | } 57 | 58 | # Check Elevation 59 | if (!(IsAdministrator)) { 60 | "Please restart this script from an administrative PowerShell!" 61 | "We cannot install the build tools without administrative rights." 62 | return 63 | } 64 | 65 | # Print Arguments 66 | Write-Output "Passed arguments: $args" 67 | 68 | if ($InstallBuildTools.IsPresent) { 69 | runInstaller; 70 | } 71 | 72 | if ($InstallPython.IsPresent) { 73 | runPythonInstaller; 74 | } 75 | -------------------------------------------------------------------------------- /ps1/set-environment.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [string]$pythonPath, 4 | [string]$pythonExePath, 5 | [string]$VisualStudioVersion, 6 | [switch]$ConfigurePython, 7 | [switch]$ConfigureBuildTools 8 | ) 9 | 10 | function configureBuildTools() { 11 | if ($VisualStudioVersion -eq "2015") { 12 | # Setting MSVS version is needed only for the VS2015 Build Tools, not for other editions 13 | [Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User") 14 | npm config set msvs_version 2015 15 | } else { 16 | # Rely on node-gyp/gyp autodetection 17 | npm config delete msvs_version 18 | npm config delete msvs_version --global 19 | [Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", $null, "User") 20 | [Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", $null, "Machine") 21 | } 22 | } 23 | 24 | function configurePython() { 25 | # Setting python path 26 | npm config set python $pythonExePath 27 | 28 | # Add Python to path 29 | [System.Environment]::SetEnvironmentVariable("Path", "$pythonPath;" + [System.Environment]::GetEnvironmentVariable("Path", "User"), "User") 30 | [System.Environment]::SetEnvironmentVariable("Path", "$pythonPath;$env:Path", "Process") 31 | } 32 | 33 | if ($ConfigureBuildTools.IsPresent) { 34 | configureBuildTools; 35 | } 36 | 37 | if ($ConfigurePython.IsPresent) { 38 | configurePython; 39 | } 40 | -------------------------------------------------------------------------------- /src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'string-width'; 2 | declare module 'nugget'; 3 | declare module 'in-gfw'; 4 | -------------------------------------------------------------------------------- /src/aquire-installers.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { OFFLINE_PATH } from './constants'; 4 | import { download } from './download'; 5 | import { log } from './logging'; 6 | import { copyInstallers } from './offline'; 7 | 8 | 9 | /** 10 | * Aquire the installers, either by copying them from 11 | * their offline location or by downloading them. 12 | * 13 | * @param {() => void} cb\ 14 | * @returns {Promise.void} 15 | */ 16 | export async function aquireInstallers(cb: () => void): Promise { 17 | const handleFailure = (error: Error) => { 18 | log(chalk.bold.red(`Downloading installers failed. Error:`), error); 19 | log(chalk.bold.red(`windows-build-tools will now exit.`)); 20 | 21 | process.exit(1); 22 | }; 23 | 24 | if (OFFLINE_PATH) { 25 | try { 26 | await copyInstallers(); 27 | 28 | cb(); 29 | } catch (error) { 30 | handleFailure(error); 31 | } 32 | } else { 33 | try { 34 | await download(cb); 35 | } catch (error) { 36 | handleFailure(error); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/compatible.js: -------------------------------------------------------------------------------- 1 | module.export = (function () { 2 | if (process.version === 'v7.1.0') { 3 | const { warn } = require('./logging') 4 | 5 | warn('--------------------------------------------------------------') 6 | warn('You are running Node v7.1.0, which has a known bug on Windows,') 7 | warn('breaking Node applications using the utils (Powershell/CMD).') 8 | warn('Please upgrade to a newer version or use Node v7.0.0.\n\n') 9 | warn('Visit https://github.com/nodejs/node/issues/9542 for details.\n') 10 | warn('windows-build-tools will now run, but might fail.') 11 | warn('---------------------------------------------------------------') 12 | } 13 | 14 | if (!/^win/.test(process.platform)) { 15 | throw new Error('This script upgrades npm on Windows, but the OS is not Windows.') 16 | } 17 | }()) 18 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import * as inGFW from 'in-gfw'; 2 | import * as path from 'path'; 3 | 4 | import { getIsPythonInstalled } from './utils/get-is-python-installed'; 5 | 6 | const pythonMirror = process.env.npm_config_python_mirror 7 | || process.env.PYTHON_MIRROR 8 | || (inGFW.osSync() ? 'https://npm.taobao.org/mirrors/python' : 'https://www.python.org/ftp/python'); 9 | 10 | // To implement 11 | export const IS_BUILD_TOOLS_INSTALLED = false; 12 | 13 | export const OFFLINE_PATH = process.env.npm_config_offline_installers; 14 | 15 | export const IS_DRY_RUN = !!process.env.npm_config_dry_run_only; 16 | 17 | export const INSTALLED_PYTHON_VERSION = getIsPythonInstalled(); 18 | 19 | export const IS_PYTHON_INSTALLED = !!INSTALLED_PYTHON_VERSION; 20 | 21 | export const PYTHON = process.arch === 'x64' 22 | ? { 23 | installerName: 'python-3.8.1.amd64.msi', 24 | installerUrl: pythonMirror.replace(/\/*$/, '/3.8.1/python-3.8.1.amd64.msi'), 25 | targetName: 'python38', 26 | logName: 'python-log.txt' 27 | } : { 28 | installerName: 'python-3.8.1.msi', 29 | installerUrl: pythonMirror.replace(/\/*$/, '/3.8.1/python-3.8.1.msi'), 30 | targetName: 'python38', 31 | logName: 'python-log.txt' 32 | }; 33 | 34 | export const BUILD_TOOLS = getBuildTools(); 35 | function getBuildTools() { 36 | const vs2017 = { 37 | installerName: 'vs_BuildTools.exe', 38 | installerUrl: 'https://download.visualstudio.microsoft.com/download/pr/11503713/e64d79b40219aea618ce2fe10ebd5f0d/vs_BuildTools.exe', 39 | logName: null, 40 | version: 2017 41 | }; 42 | const vs2015 = { 43 | installerName: 'BuildTools_Full.exe', 44 | installerUrl: 'https://download.microsoft.com/download/5/f/7/5f7acaeb-8363-451f-9425-68a90f98b238/visualcppbuildtools_full.exe', 45 | logName: 'build-tools-log.txt', 46 | version: 2015 47 | }; 48 | 49 | if (process.env.npm_config_vs2017) { 50 | return vs2017; 51 | } else if (process.env.npm_config_vs2015) { 52 | return vs2015; 53 | } 54 | 55 | // Default 56 | return vs2017; 57 | } 58 | 59 | export const installerScriptPath = IS_DRY_RUN 60 | ? path.join(__dirname, '..', 'ps1', 'dry-run.ps1') 61 | : path.join(__dirname, '..', 'ps1', 'launch-installer.ps1'); 62 | -------------------------------------------------------------------------------- /src/download.ts: -------------------------------------------------------------------------------- 1 | import * as nugget from 'nugget'; 2 | 3 | import chalk from 'chalk'; 4 | import { IS_BUILD_TOOLS_INSTALLED, IS_DRY_RUN, IS_PYTHON_INSTALLED } from './constants'; 5 | import { Installer } from './interfaces'; 6 | import { log } from './logging'; 7 | import { getBuildToolsInstallerPath } from './utils/get-build-tools-installer-path'; 8 | import { getPythonInstallerPath } from './utils/get-python-installer-path'; 9 | 10 | /** 11 | * Downloads the Visual Studio C++ Build Tools and Python installer to a temporary folder 12 | * at %USERPROFILE%\.windows-build-tools 13 | */ 14 | export async function download(cb: () => void) { 15 | const handleFailure = (error: Error, name: string) => { 16 | log(chalk.bold.red(`Downloading ${name} failed. Error:`), error); 17 | log(chalk.bold.red(`windows-build-tools will now exit.`)); 18 | process.exit(1); 19 | }; 20 | 21 | if (!IS_PYTHON_INSTALLED) { 22 | try { 23 | await downloadTools(getPythonInstallerPath()); 24 | } catch (error) { 25 | handleFailure(error, 'Python'); 26 | } 27 | } 28 | 29 | if (!IS_BUILD_TOOLS_INSTALLED) { 30 | try { 31 | await downloadTools(getBuildToolsInstallerPath()); 32 | } catch (error) { 33 | handleFailure(error, 'Visual Studio Build Tools'); 34 | } 35 | } 36 | 37 | cb(); 38 | } 39 | 40 | /** 41 | * Downloads specified file with a url from the installer. 42 | */ 43 | function downloadTools(installer: Installer): Promise { 44 | return new Promise((resolve, reject) => { 45 | const nuggetOptions = { 46 | target: installer.fileName, 47 | dir: installer.directory, 48 | resume: process.env.npm_config_resume || true, 49 | verbose: true, 50 | strictSSL: process.env.npm_config_strict_ssl || false, 51 | proxy: process.env.npm_config_proxy || process.env.PROXY || undefined, 52 | sockets: process.env.npm_config_sockets || undefined 53 | }; 54 | 55 | const nuggetCallback = (errors?: Array) => { 56 | if (errors) { 57 | // nugget returns an array of errors but we only need 1st because we only have 1 url 58 | const error = errors[0]; 59 | 60 | if (error.message.indexOf('404') === -1) { 61 | return reject(error); 62 | } else { 63 | return reject(new Error(`Could not find ${installer.fileName} at ${installer.url}`)); 64 | } 65 | } 66 | 67 | log(`Downloaded ${installer.fileName}. Saved to ${installer.path}.`); 68 | resolve(installer.path); 69 | }; 70 | 71 | if (IS_DRY_RUN) { 72 | nuggetCallback(); 73 | } else { 74 | // Log double newline because Nugget is the worst about overwriting 75 | // output 76 | log('\n'); 77 | nugget(installer.url, nuggetOptions, nuggetCallback); 78 | } 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as path from 'path'; 3 | 4 | import { BUILD_TOOLS, IS_DRY_RUN } from './constants'; 5 | import { InstallationDetails } from './interfaces'; 6 | import { log } from './logging'; 7 | import { executeChildProcess } from './utils/execute-child-process'; 8 | 9 | const debug = require('debug')('windows-build-tools'); 10 | 11 | /** 12 | * Uses PowerShell to configure the environment for 13 | * msvs_version 2015 and npm python 3.8 14 | * 15 | * @params variables an object with paths for different environmental variables 16 | */ 17 | export function setEnvironment(env: InstallationDetails): Promise { 18 | const scriptPath = IS_DRY_RUN 19 | ? path.join(__dirname, '..', 'ps1', 'dry-run.ps1') 20 | : path.join(__dirname, '..', 'ps1', 'set-environment.ps1'); 21 | 22 | let pythonArguments = ''; 23 | let buildArguments = ''; 24 | 25 | // Should we configure Python? 26 | if (env.python.toConfigure) { 27 | const pythonPath = path.join(env.python.installPath); 28 | const pythonExePath = path.join(pythonPath, 'python.exe'); 29 | 30 | pythonArguments += ` -ConfigurePython -pythonPath '${pythonPath}' -pythonExePath '${pythonExePath}'`; 31 | } 32 | 33 | // Should we configure the VS Build Tools? 34 | if (env.buildTools.toConfigure) { 35 | const vccParam = `-VisualStudioVersion '${BUILD_TOOLS.version.toString()}'`; 36 | buildArguments += ` -ConfigureBuildTools ${vccParam}`; 37 | } 38 | 39 | // Log what we're doing 40 | if (pythonArguments && buildArguments) { 41 | log(chalk.bold.green(`Now configuring the Visual Studio Build Tools and Python...`)); 42 | } else if (pythonArguments) { 43 | log(chalk.bold.green(`Now configuring Python...`)); 44 | } else if (buildArguments) { 45 | log(chalk.bold.green(`Now configuring the Visual Studio Build Tools..`)); 46 | } else { 47 | log(chalk.bold.green(`Skipping configuration: No configuration for Python or Visual Studio Build Tools required.`)); 48 | return(new Promise((resolve, reject) => resolve())) 49 | .then(() => log(chalk.bold.green(`\nAll done!\n`))); 50 | } 51 | 52 | const maybeArgs = `${pythonArguments}${buildArguments}`; 53 | const psArgs = `& {& '${scriptPath}' ${maybeArgs} }`; 54 | const args = ['-ExecutionPolicy', 'Bypass', '-NoProfile', '-NoLogo', psArgs]; 55 | 56 | return executeChildProcess('powershell.exe', args) 57 | .then(() => log(chalk.bold.green(`\nAll done!\n`))) 58 | .catch((error) => debug(`Encountered environment setting error: ${error}`)); 59 | } 60 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Set verbose mode 2 | if (process.env.npm_config_debug) { 3 | process.env.DEBUG = '*'; 4 | } 5 | 6 | require('./compatible'); 7 | require('./start'); 8 | -------------------------------------------------------------------------------- /src/install/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { INSTALLED_PYTHON_VERSION, IS_PYTHON_INSTALLED } from '../constants'; 4 | import { InstallationDetails, InstallationReport } from '../interfaces'; 5 | import { log, shouldLog } from '../logging'; 6 | import { cleanExistingLogFiles } from '../utils/clean'; 7 | import { getBuildToolsInstallerPath } from '../utils/get-build-tools-installer-path'; 8 | import { getPythonInstallerPath } from '../utils/get-python-installer-path'; 9 | import { getWorkDirectory } from '../utils/get-work-dir'; 10 | import { removePath } from '../utils/remove-path'; 11 | import { createSingleLineLogger } from '../utils/single-line-log'; 12 | import { launchInstaller } from './launch'; 13 | import { Tailer } from './tailer'; 14 | 15 | const singleLineLogger = createSingleLineLogger(); 16 | const vccInstaller = getBuildToolsInstallerPath(); 17 | 18 | const vcLogTitle = chalk.bold.green('---------- Visual Studio Build Tools ----------'); 19 | const pyLogTitle = chalk.bold.green('------------------- Python --------------------'); 20 | let vccLastLines = [ 'Still waiting for installer log file...' ]; 21 | let pythonLastLines = [ 'Still waiting for installer log file...' ]; 22 | let lastLinesInterval = null; 23 | 24 | const debug = require('debug')('windows-build-tools'); 25 | 26 | /** 27 | * Installs the build tools, tailing the installation log file 28 | * to understand what's happening 29 | */ 30 | 31 | export function install(cb: (details: InstallationDetails) => void) { 32 | log(chalk.green('\nStarting installation...')); 33 | 34 | cleanExistingLogFiles(); 35 | removePath(); 36 | 37 | launchInstaller() 38 | .then(() => launchLog()) 39 | .then(() => Promise.all([ tailBuildInstallation(), tailPythonInstallation() ])) 40 | .then((details: [ InstallationReport, InstallationReport ]) => { 41 | cb({ buildTools: details[0], python: details[1] }); 42 | }) 43 | .catch((error) => { 44 | log(error); 45 | }); 46 | } 47 | 48 | function logStatus() { 49 | const updatedLog = [ vcLogTitle, ...vccLastLines, pyLogTitle, ...pythonLastLines ]; 50 | 51 | // We expect a length of 16 52 | if (updatedLog.length < 16) { 53 | updatedLog.fill('', updatedLog.length, 16); 54 | } 55 | 56 | if (debug.enabled) { 57 | updatedLog.forEach((s) => debug(s)); 58 | } else { 59 | singleLineLogger.log(updatedLog.join('\n')); 60 | } 61 | } 62 | 63 | function launchLog() { 64 | if (!shouldLog) return; 65 | 66 | log('Launched installers, now waiting for them to finish.'); 67 | log('This will likely take some time - please be patient!\n'); 68 | log('Status from the installers:'); 69 | 70 | lastLinesInterval = setInterval(logStatus, 500); 71 | } 72 | 73 | function stopLog() { 74 | if (!shouldLog) return; 75 | 76 | clearInterval(lastLinesInterval); 77 | 78 | // Flush newlines 79 | log('\n'); 80 | } 81 | 82 | function tailBuildInstallation(): Promise { 83 | return new Promise((resolve, reject) => { 84 | const tailer = new Tailer(vccInstaller.logPath); 85 | 86 | tailer.on('lastLines', (lastLines) => { 87 | vccLastLines = lastLines; 88 | }); 89 | 90 | tailer.on('exit', (result, details) => { 91 | debug('Install: Build tools tailer exited'); 92 | 93 | if (result === 'error') { 94 | debug('Installer: Tailer found error with installer', details); 95 | reject(new Error(`Found error with VCC installer: ${details}`)); 96 | } 97 | 98 | if (result === 'success') { 99 | vccLastLines = [ chalk.bold.green('Successfully installed Visual Studio Build Tools.') ]; 100 | debug('Installer: Successfully installed Visual Studio Build Tools according to tailer'); 101 | resolve({ success: true, toConfigure: true }); 102 | } 103 | 104 | // Stop the log now. If we need to report failures, we need 105 | // to do it after the single-line-logger has stopped messing 106 | // with the terminal. 107 | logStatus(); 108 | stopLog(); 109 | 110 | if (result === 'failure') { 111 | log(chalk.bold.red('\nCould not install Visual Studio Build Tools.')); 112 | log('Please find more details in the log files, which can be found at'); 113 | log(getWorkDirectory() + '\n'); 114 | debug('Installer: Failed to install according to tailer'); 115 | resolve({ success: false }); 116 | } 117 | }); 118 | 119 | tailer.start(); 120 | }); 121 | } 122 | 123 | function tailPythonInstallation(): Promise { 124 | return new Promise((resolve, reject) => { 125 | if (IS_PYTHON_INSTALLED) { 126 | debug('Installer: Python is already installed'); 127 | pythonLastLines = [ chalk.bold.green(`${INSTALLED_PYTHON_VERSION} is already installed, not installing again.`) ]; 128 | 129 | return resolve({ toConfigure: false, installPath: '', success: true }); 130 | } 131 | 132 | // The log file for msiexe is utf-16 133 | const tailer = new Tailer(getPythonInstallerPath().logPath, 'ucs2'); 134 | 135 | tailer.on('lastLines', (lastLines) => { 136 | pythonLastLines = lastLines; 137 | }); 138 | 139 | tailer.on('exit', (result, details) => { 140 | debug('python tailer exited'); 141 | if (result === 'error') { 142 | debug('Installer: Tailer found error with installer', details); 143 | reject(new Error(`Found error with Python installer: ${details}`)); 144 | } 145 | 146 | if (result === 'success') { 147 | pythonLastLines = [ chalk.bold.green('Successfully installed Python 3.8') ]; 148 | 149 | debug('Installer: Successfully installed Python 3.8 according to tailer'); 150 | resolve({ 151 | installPath: details || getPythonInstallerPath().targetPath, 152 | toConfigure: true, 153 | success: true 154 | }); 155 | } 156 | 157 | if (result === 'failure') { 158 | log(chalk.bold.red('\nCould not install Python 3.8.')); 159 | log('Please find more details in the log files, which can be found at'); 160 | log(getWorkDirectory() + '\n'); 161 | 162 | debug('Installer: Failed to install Python 3.8 according to tailer'); 163 | resolve({ 164 | success: false 165 | }); 166 | } 167 | }); 168 | 169 | tailer.start(); 170 | }); 171 | } 172 | -------------------------------------------------------------------------------- /src/install/launch.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { spawn } from 'child_process'; 3 | 4 | import { 5 | BUILD_TOOLS, 6 | IS_BUILD_TOOLS_INSTALLED, 7 | IS_PYTHON_INSTALLED, 8 | installerScriptPath 9 | } from '../constants'; 10 | import { log } from '../logging'; 11 | import { getBuildToolsInstallerPath } from '../utils/get-build-tools-installer-path'; 12 | import { getBuildToolsExtraParameters } from '../utils/get-build-tools-parameters'; 13 | import { getPythonInstallerPath } from '../utils/get-python-installer-path'; 14 | 15 | const debug = require('debug')('windows-build-tools'); 16 | 17 | const vccInstaller = getBuildToolsInstallerPath(); 18 | const pythonInstaller = getPythonInstallerPath(); 19 | 20 | /** 21 | * Launches the installer, using a PS1 script as a middle-man 22 | * 23 | * @returns {Promise} - Promise that resolves once done 24 | */ 25 | export function launchInstaller(): Promise { 26 | return new Promise((resolve, reject) => { 27 | const vccParam = `-VisualStudioVersion '${BUILD_TOOLS.version.toString()}'`; 28 | const pathParam = `-BuildToolsInstallerPath '${vccInstaller.directory}'`; 29 | 30 | const buildToolsParam = IS_BUILD_TOOLS_INSTALLED 31 | ? `` 32 | : `-InstallBuildTools -ExtraBuildToolsParameters '${getBuildToolsExtraParameters()}'`; 33 | 34 | const pythonParam = IS_PYTHON_INSTALLED 35 | ? `` 36 | : `-PythonInstaller '${pythonInstaller.fileName}' -InstallPython`; 37 | 38 | const psArgs = `& {& '${installerScriptPath}' ${pathParam} ${buildToolsParam} ${pythonParam} ${vccParam} }`; 39 | const args = ['-ExecutionPolicy', 'Bypass', '-NoProfile', '-NoLogo', psArgs]; 40 | 41 | debug(`Installer: Launching installer in ${vccInstaller.directory} with parameters ${args}.`); 42 | 43 | let child; 44 | 45 | try { 46 | child = spawn('powershell.exe', args); 47 | } catch (error) { 48 | log(chalk.bold.red('Error: failed while trying to run powershell.exe.')); 49 | log(chalk.bold.greenBright('Hint: Is "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0" in your system path?')); 50 | log(`\nTried to execute: "powershell.exe ${args.join(' ')}"\n`); 51 | 52 | return reject(error); 53 | } 54 | 55 | child.stdout.on('data', (data) => { 56 | debug(`Installer: Stdout from launch-installer.ps1: ${data.toString()}`); 57 | 58 | if (data.toString().includes('Please restart this script from an administrative PowerShell!')) { 59 | log(chalk.bold.red('Please restart this script from an administrative PowerShell!')); 60 | log('The build tools cannot be installed without administrative rights.'); 61 | log('To fix, right-click on PowerShell and run "as Administrator".'); 62 | 63 | // Bail out 64 | process.exit(1); 65 | } 66 | }); 67 | 68 | child.stderr.on('data', (data) => debug(`Installer: Stderr from launch-installer.ps1: ${data.toString()}`)); 69 | 70 | child.on('exit', () => resolve()); 71 | child.stdin.end(); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/install/tailer.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import * as fs from 'fs-extra'; 3 | 4 | import { IS_DRY_RUN } from '../constants'; 5 | import { findVCCLogFile } from '../utils/find-logfile'; 6 | import { includesFailure, includesSuccess } from '../utils/installation-sucess'; 7 | 8 | const debug = require('debug')('windows-build-tools'); 9 | 10 | export class Tailer extends EventEmitter { 11 | public logFile: string; 12 | public encoding: string; 13 | public tailInterval; 14 | 15 | constructor(logfile: string, encoding: string = 'utf8') { 16 | super(); 17 | 18 | this.logFile = logfile; 19 | this.encoding = encoding; 20 | } 21 | 22 | /** 23 | * Starts watching a the logfile 24 | */ 25 | public start() { 26 | if (this.logFile) { 27 | debug(`Tail: Waiting for log file to appear in ${this.logFile}`); 28 | } else { 29 | debug(`Tail: Waiting for log file to appear. Searching in %TEMP%`); 30 | } 31 | this.waitForLogFile(); 32 | } 33 | 34 | /** 35 | * Stop watching 36 | */ 37 | public stop(...args: Array) { 38 | debug(`Tail: Stopping`, ...args); 39 | this.emit('exit', ...args); 40 | clearInterval(this.tailInterval); 41 | } 42 | 43 | /** 44 | * Start tailing things 45 | */ 46 | public tail() { 47 | debug(`Tail: Tailing ${this.logFile}`); 48 | 49 | this.tailInterval = setInterval(() => this.readData(), 500); 50 | } 51 | 52 | public readData() { 53 | if (IS_DRY_RUN) { 54 | this.emit('lastLines', `Dry run, we're all done`); 55 | return this.stop('success'); 56 | } 57 | 58 | let data = ''; 59 | 60 | // Read the log file 61 | try { 62 | data = fs.readFileSync(this.logFile, this.encoding); 63 | } catch (err) { 64 | debug(`Tail start: Could not read logfile ${this.logFile}: ${err}`); 65 | return; 66 | } 67 | 68 | if (data && data.length > 0) { 69 | const split = data.split(/\r?\n/) || [ 'Still looking for log file...' ]; 70 | const lastLines = split 71 | .filter((l) => l.trim().length > 0) 72 | .slice(split.length - 6, split.length); 73 | 74 | this.emit('lastLines', lastLines); 75 | this.handleData(data); 76 | } 77 | } 78 | 79 | /** 80 | * Handle data and see if there's something we'd like to report 81 | * 82 | * @param {string} data 83 | */ 84 | public handleData(data: string) { 85 | // Handle Success 86 | const { isBuildToolsSuccess, isPythonSuccess } = includesSuccess(data); 87 | 88 | if (isBuildToolsSuccess) { 89 | debug(`Tail: Reporting success for VCC Build Tools`); 90 | this.stop('success'); 91 | return; 92 | } 93 | 94 | if (isPythonSuccess) { 95 | // Finding the python installation path from the log file 96 | const matches = data.match(/Property\(S\): TARGETDIR = (.*)\r\n/); 97 | let pythonPath; 98 | 99 | if (matches) { 100 | pythonPath = matches[1]; 101 | } 102 | 103 | debug(`Tail: Reporting success for Python`); 104 | this.stop('success', pythonPath); 105 | return; 106 | } 107 | 108 | // Handle Failure 109 | const { isPythonFailure, isBuildToolsFailure } = includesFailure(data); 110 | 111 | if (isPythonFailure || isBuildToolsFailure) { 112 | debug(`Tail: Reporting failure in ${this.logFile}`); 113 | this.stop('failure'); 114 | } 115 | } 116 | 117 | /** 118 | * Waits for a given file, resolving when it's available 119 | * 120 | * @param file {string} - Path to file 121 | * @returns {Promise.} - Promise resolving with fs.stats object 122 | */ 123 | public waitForLogFile() { 124 | if (IS_DRY_RUN) return this.tail(); 125 | 126 | const handleStillWaiting = () => { 127 | debug('Tail: waitForFile: still waiting'); 128 | setTimeout(this.waitForLogFile.bind(this), 2000); 129 | }; 130 | 131 | const handleKnownPath = (logFile) => { 132 | fs.lstat(logFile, (err, stats) => { 133 | if (err && err.code === 'ENOENT') { 134 | handleStillWaiting(); 135 | } else if (err) { 136 | debug('Tail: waitForFile: Unexpected error', err); 137 | throw err; 138 | } else { 139 | debug(`Tail: waitForFile: Found ${logFile}`); 140 | this.tail(); 141 | } 142 | }); 143 | }; 144 | 145 | // If don't have a logfile, we need to find one. The only one 146 | // we need to find right now is the VCC 2017 logfile. 147 | if (!this.logFile) { 148 | findVCCLogFile() 149 | .then((logFile) => { 150 | debug(`Tail: LogFile found: ${logFile}`); 151 | 152 | if (!logFile) { 153 | handleStillWaiting(); 154 | } else { 155 | this.logFile = logFile; 156 | handleKnownPath(logFile); 157 | } 158 | }) 159 | .catch((error) => { 160 | throw new Error(error); 161 | }); 162 | } else { 163 | handleKnownPath(this.logFile); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Installer { 2 | fileName: string; 3 | directory: string; 4 | url: string; 5 | path: string; 6 | } 7 | 8 | export interface InstallationDetails { 9 | buildTools: InstallationReport; 10 | python: InstallationReport; 11 | } 12 | 13 | export interface InstallationReport { 14 | success: boolean; 15 | toConfigure?: boolean; 16 | installPath?: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/logging.ts: -------------------------------------------------------------------------------- 1 | export const shouldLog = !process.env.npm_config_disable_logging; 2 | 3 | /** 4 | * Log, unless logging is disabled. Parameters identical with console.log. 5 | */ 6 | export function log(...args: Array) { 7 | if (shouldLog) { 8 | console.log.apply(this, args); 9 | } 10 | } 11 | 12 | /** 13 | * Warn, unless logging is disabled. Parameters identical with console.error. 14 | */ 15 | export function warn(...args: Array) { 16 | if (shouldLog) { 17 | console.warn.apply(this, args); 18 | } 19 | } 20 | 21 | /** 22 | * Error, unless logging is disabled. Parameters identical with console.error. 23 | */ 24 | export function error(...args: Array) { 25 | if (shouldLog) { 26 | console.error.apply(this, args); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/offline.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | import * as path from 'path'; 3 | import { BUILD_TOOLS, IS_DRY_RUN, OFFLINE_PATH, PYTHON } from './constants'; 4 | import { error, log } from './logging'; 5 | import { getBuildToolsInstallerPath } from './utils/get-build-tools-installer-path'; 6 | import { getPythonInstallerPath } from './utils/get-python-installer-path'; 7 | 8 | const PYTHON_INSTALLER = path.join(OFFLINE_PATH || '', PYTHON.installerName); 9 | const VS_INSTALLER = path.join(OFFLINE_PATH || '', BUILD_TOOLS.installerName); 10 | 11 | /** 12 | * Check if the installer at a given path can be found and error out if 13 | * it does not exist. 14 | * 15 | * @param {string} installerPath 16 | * @param {string} installerName 17 | * @returns {boolean} 18 | */ 19 | function ensureInstaller(installerPath: string, installerName: string): void { 20 | if (!fs.existsSync(installerPath)) { 21 | if (IS_DRY_RUN) { 22 | log(`Dry run: Installer ${installerPath} not found, would have stopped here.`); 23 | return; 24 | } 25 | 26 | let message = `Offline installation: Offline path ${OFFLINE_PATH} was passed, `; 27 | message += `but we could not find ${installerName} in that path. `; 28 | message += `Aborting installation now.`; 29 | 30 | error(message); 31 | 32 | process.exit(1); 33 | } 34 | } 35 | 36 | /** 37 | * Copy the installers from their offline directory to their target directory. 38 | * 39 | * @returns {Promise.void} 40 | */ 41 | export async function copyInstallers(): Promise { 42 | if (!OFFLINE_PATH) { 43 | throw new Error(`npm_config_offline_installers not found!`); 44 | } 45 | 46 | ensureInstaller(PYTHON_INSTALLER, PYTHON.installerName); 47 | ensureInstaller(VS_INSTALLER, BUILD_TOOLS.installerName); 48 | 49 | if (IS_DRY_RUN) { 50 | log(`Dry run: Would have copied installers.`); 51 | return; 52 | } 53 | 54 | try { 55 | await fs.copy(PYTHON_INSTALLER, getPythonInstallerPath().path); 56 | await fs.copy(VS_INSTALLER, getBuildToolsInstallerPath().path); 57 | } catch (error) { 58 | let message = `Offline installation: Could not copy over installers. `; 59 | message += `Aborting installation now.\n`; 60 | message += error; 61 | 62 | error(message); 63 | 64 | process.exit(1); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/start.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { aquireInstallers } from './aquire-installers'; 4 | import { IS_DRY_RUN } from './constants'; 5 | import { setEnvironment } from './environment'; 6 | import { install } from './install'; 7 | import { InstallationDetails } from './interfaces'; 8 | import { log } from './logging'; 9 | 10 | function main() { 11 | if (IS_DRY_RUN) { 12 | log(chalk.bold.green(`Dry run: Not actually doing anything.`)); 13 | } 14 | 15 | // The dumbest callbacks. All other methods resulted 16 | // in stacks that we're too deep and errored out on some 17 | // machines. 18 | aquireInstallers(() => { 19 | install((variables: InstallationDetails) => { 20 | setEnvironment(variables) 21 | .then(() => process.exit(0)) 22 | .catch(() => process.exit(1)); 23 | }); 24 | }) 25 | .catch(console.error); 26 | } 27 | 28 | main(); 29 | -------------------------------------------------------------------------------- /src/utils/clean.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { getBuildToolsInstallerPath } from './get-build-tools-installer-path'; 3 | import { getPythonInstallerPath } from './get-python-installer-path'; 4 | 5 | /** 6 | * Cleans existing log files 7 | */ 8 | export function cleanExistingLogFiles() { 9 | const files = [ getBuildToolsInstallerPath().logPath, getPythonInstallerPath().logPath ]; 10 | 11 | files.forEach((file) => { 12 | if (fs.existsSync(file)) { 13 | fs.unlinkSync(file); 14 | } 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/ensure-windows.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Ensures that the currently running platform is Windows, 3 | * exiting the process if it is not 4 | */ 5 | export function ensureWindows() { 6 | if (process.platform !== 'win32') { 7 | console.log('This tool requires Windows.\n'); 8 | process.exit(1); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/execute-child-process.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | /** 4 | * Starts a child process using the provided executable 5 | * 6 | * @param fileName - Path to the executable to start 7 | */ 8 | export function executeChildProcess(fileName: string, args: Array): Promise { 9 | return new Promise((resolve, reject) => { 10 | const child = spawn(fileName, args); 11 | 12 | child.on('exit', (code) => { 13 | if (code !== 0) { 14 | return reject(new Error(fileName + ' exited with code: ' + code)); 15 | } 16 | 17 | return resolve(); 18 | }); 19 | 20 | child.stdin.end(); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/find-logfile.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | import { tmpdir } from 'os'; 3 | import * as path from 'path'; 4 | 5 | const debug = require('debug')('windows-build-tools'); 6 | const tmp = tmpdir(); 7 | 8 | /** 9 | * Looks for a dd_client_ file and returns the path if found.\ 10 | * Returns null if not found. 11 | * 12 | * @returns {string|null} 13 | */ 14 | export function findVCCLogFile(): Promise { 15 | return new Promise((resolve) => { 16 | fs.readdir(tmp) 17 | .then((contents) => { 18 | // Files that begin with dd_client_ 19 | const matchingFiles = contents.filter((f) => f.startsWith('dd_installer_')); 20 | let matchingFile = null; 21 | 22 | if (matchingFiles && matchingFiles.length === 1) { 23 | // Is it just one? Cool, let's use that one 24 | matchingFile = path.join(tmp, matchingFiles[0]); 25 | debug(`Find LogFile: Just one file found, resolving with ${matchingFile}`); 26 | } else if (!matchingFiles || matchingFiles.length === 0) { 27 | // No files? Return null 28 | debug(`Find LogFile: No files found, resolving with null`); 29 | matchingFile = null; 30 | } else { 31 | // Multiple files! Oh boy, let's find the last one 32 | debug(`Find LogFile: Multiple files found, determining last modified one`); 33 | const lastModified = matchingFiles.reduce((previous, current) => { 34 | const file = path.join(tmp, current); 35 | const stats = fs.statSync(file); 36 | 37 | let modifiedTime; 38 | 39 | if (stats && (stats as any).mtimeMs) { 40 | // This value is only available in Node 8+ 41 | modifiedTime = (stats as any).mtimeMs; 42 | } else if (stats && stats.mtime) { 43 | // Fallback for the other versions 44 | modifiedTime = new Date(stats.mtime).getTime(); 45 | } 46 | 47 | debug(`Find LogFile: Comparing ${modifiedTime} to ${previous.timestamp}`); 48 | 49 | if (modifiedTime && modifiedTime > previous.timestamp) { 50 | return { file: current, timestamp: modifiedTime }; 51 | } else { 52 | return previous; 53 | } 54 | }, { file: null, timestamp: 0 }); 55 | 56 | debug(`Find LogFile: Returning ${lastModified.file}`); 57 | matchingFile = path.join(tmp, lastModified.file); 58 | } 59 | 60 | resolve(matchingFile); 61 | }) 62 | .catch((error) => { 63 | debug(`Did not find VCC logfile: ${error}`); 64 | return null; 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/get-build-tools-installer-path.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { BUILD_TOOLS } from '../constants'; 4 | import { getWorkDirectory } from './get-work-dir'; 5 | 6 | /** 7 | * Ensures that %USERPROFILE%/.windows-build-tools exists 8 | * and returns the path to it 9 | * 10 | * @returns {Object} - Object containing path and fileName of installer 11 | */ 12 | export function getBuildToolsInstallerPath() { 13 | const directory = getWorkDirectory(); 14 | 15 | return { 16 | path: path.join(directory, BUILD_TOOLS.installerName), 17 | fileName: BUILD_TOOLS.installerName, 18 | url: BUILD_TOOLS.installerUrl, 19 | logPath: BUILD_TOOLS.logName ? path.join(directory, BUILD_TOOLS.logName) : null, 20 | directory 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/get-build-tools-parameters.ts: -------------------------------------------------------------------------------- 1 | import { BUILD_TOOLS } from '../constants'; 2 | 3 | const debug = require('debug')('windows-build-tools'); 4 | 5 | export function getBuildToolsExtraParameters() { 6 | let extraArgs = ''; 7 | 8 | if (process.env.npm_config_vcc_build_tools_parameters) { 9 | try { 10 | const parsedArgs = JSON.parse(process.env.npm_config_vcc_build_tools_parameters); 11 | 12 | if (parsedArgs && parsedArgs.length > 0) { 13 | extraArgs = parsedArgs.join('%_; '); 14 | } 15 | } catch (e) { 16 | debug(`Installer: Parsing additional arguments for VCC build tools failed: ${e.message}`); 17 | debug(`Input received: ${process.env.npm_config_vcc_build_tools_parameters}`); 18 | } 19 | } 20 | 21 | if (!!process.env.npm_config_include_arm64_tools && BUILD_TOOLS.version === 2017) { 22 | extraArgs += ' --add Microsoft.VisualStudio.Component.VC.Tools.ARM64 --add Microsoft.VisualStudio.Component.VC.ATL.ARM64'; 23 | } 24 | 25 | return extraArgs; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/get-is-python-installed.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process'; 2 | 3 | let _isPythonInstalled: string | null | undefined; 4 | 5 | export function getIsPythonInstalled() { 6 | if (_isPythonInstalled !== undefined) return _isPythonInstalled; 7 | 8 | try { 9 | const options = { windowsHide: true, stdio: null }; 10 | const { output } = spawnSync('python', [ '-V' ], options as any); 11 | const version = output.toString().trim().replace(/,/g, ''); 12 | 13 | if (version && version.includes(' 3.')) { 14 | return _isPythonInstalled = version; 15 | } else { 16 | return _isPythonInstalled = null; 17 | } 18 | } catch (error) { 19 | return _isPythonInstalled = null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/get-python-installer-path.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { PYTHON } from '../constants'; 4 | import { getWorkDirectory } from './get-work-dir'; 5 | 6 | /** 7 | * Ensures that %USERPROFILE%/.windows-build-tools exists 8 | * and returns the path to it 9 | */ 10 | export function getPythonInstallerPath() { 11 | const directory = getWorkDirectory(); 12 | 13 | return { 14 | path: path.join(directory, PYTHON.installerName), 15 | fileName: PYTHON.installerName, 16 | url: PYTHON.installerUrl, 17 | logPath: path.join(directory, PYTHON.logName), 18 | targetPath: path.join(directory, PYTHON.targetName), 19 | directory 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/get-work-dir.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | import * as path from 'path'; 3 | 4 | /** 5 | * Ensures that %USERPROFILE%/.windows-build-tools exists 6 | * and returns the path to it 7 | * 8 | * @returns {string} - Path to windows-build-tools working dir 9 | */ 10 | export function getWorkDirectory() { 11 | const homeDir = process.env.USERPROFILE || require('os').homedir(); 12 | const workDir = path.join(homeDir, '.windows-build-tools'); 13 | 14 | try { 15 | fs.ensureDirSync(workDir); 16 | return workDir; 17 | } catch (error) { 18 | console.log(error); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/installation-sucess.ts: -------------------------------------------------------------------------------- 1 | import { BUILD_TOOLS } from '../constants'; 2 | 3 | /** 4 | * Dumb string comparison: Can we assume installation success 5 | * after taking a look at the logs? 6 | * 7 | * @param {string} [input=''] 8 | */ 9 | export function includesSuccess(input: string = '') { 10 | let isBuildToolsSuccess = false; 11 | let isPythonSuccess = false; 12 | 13 | if (BUILD_TOOLS.version === 2015) { 14 | // Success strings for build tools (2015) 15 | isBuildToolsSuccess = input.includes('Variable: IsInstalled = 1') || 16 | input.includes('Variable: BuildTools_Core_Installed = ') || 17 | input.includes('WixBundleInstalled = 1') || 18 | input.includes('Apply complete, result: 0x0, restart: None, ba requested restart:'); 19 | } else { 20 | // Success strings for build tools (2017) 21 | isBuildToolsSuccess = input.includes('Closing installer. Return code: 3010.') || 22 | input.includes('Closing installer. Return code: 0.') || input.includes('Closing the installer with exit code 0'); 23 | } 24 | 25 | // Success strings for Python 26 | isPythonSuccess = input.includes('INSTALL. Return value 1') || 27 | input.includes('Installation completed successfully') || 28 | input.includes('Configuration completed successfully'); 29 | 30 | return { 31 | isBuildToolsSuccess, 32 | isPythonSuccess 33 | }; 34 | } 35 | 36 | /** 37 | * Dumb string comparison: Can we assume installation success 38 | * after taking a look at the logs? 39 | * 40 | * @param {string} [input=''] 41 | */ 42 | export function includesFailure(input: string = '') { 43 | let isBuildToolsFailure = false; 44 | let isPythonFailure = false; 45 | 46 | isBuildToolsFailure = input.includes('Closing installer. Return code:') || 47 | input.includes('Shutting down, exit code:'); 48 | 49 | isPythonFailure = input.includes(' -- Installation failed.'); 50 | 51 | return { 52 | isBuildToolsFailure, 53 | isPythonFailure 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/remove-path.ts: -------------------------------------------------------------------------------- 1 | const savedPath = process.env.Path; 2 | 3 | /** 4 | * This patches npm bug https://github.com/npm/npm-lifecycle/issues/20, 5 | * which added more PATH variables than were allowed. 6 | * 7 | * @returns {void} 8 | */ 9 | export function removePath(): void { 10 | Object.defineProperty(process.env, 'PATH', { value: undefined }); 11 | Object.defineProperty(process.env, 'path', { value: undefined }); 12 | 13 | delete process.env.PATH; 14 | delete process.env.path; 15 | 16 | process.env.Path = savedPath; 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/single-line-log.ts: -------------------------------------------------------------------------------- 1 | import * as stringWidth from 'string-width'; 2 | 3 | const MOVE_LEFT = Buffer.from('1b5b3130303044', 'hex').toString(); 4 | const MOVE_UP = Buffer.from('1b5b3141', 'hex').toString(); 5 | const CLEAR_LINE = Buffer.from('1b5b304b', 'hex').toString(); 6 | const stream = process.stdout; 7 | 8 | export function createSingleLineLogger() { 9 | const write = stream.write; 10 | let str; 11 | 12 | stream.write = function(data: string) { 13 | if (str && data !== str) str = null; 14 | return write.apply(this, arguments); 15 | }; 16 | 17 | process.on('exit', () => { 18 | if (str !== null) stream.write(''); 19 | }); 20 | 21 | let prevLineCount = 0; 22 | 23 | const log = function(...args: Array) { 24 | str = ''; 25 | const nextStr = Array.prototype.join.call(args, ' '); 26 | 27 | // Clear screen 28 | for (let i = 0; i < prevLineCount; i++) { 29 | str += MOVE_LEFT + CLEAR_LINE + (i < prevLineCount - 1 ? MOVE_UP : ''); 30 | } 31 | 32 | // Actual log output 33 | str += nextStr; 34 | stream.write(str); 35 | 36 | // How many lines to remove on next clear screen 37 | const prevLines = nextStr.split('\n'); 38 | prevLineCount = 0; 39 | 40 | for (const prevLine of prevLines) { 41 | prevLineCount += Math.ceil(stringWidth(prevLine) / stream.columns) || 1; 42 | } 43 | }; 44 | 45 | const clear = () => stream.write(''); 46 | 47 | return { log, clear }; 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "allowJs": true, 5 | "lib": ["es2017"], 6 | "target": "es2017", 7 | "alwaysStrict": true, 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "baseUrl": ".", 12 | "paths": { 13 | "*" : ["./node_modules/@types/*", "*"] 14 | } 15 | }, 16 | "include": [ 17 | "src/**/*" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest" 4 | ], 5 | "rules": { 6 | "await-promise": true, 7 | "curly": false, 8 | "eofline": false, 9 | "align": [ 10 | true, 11 | "parameters" 12 | ], 13 | "array-type": [ 14 | true, 15 | "generic" 16 | ], 17 | "arrow-parens": true, 18 | "class-name": true, 19 | "comment-format": [ 20 | false 21 | ], 22 | "indent": [ 23 | true, 24 | "spaces" 25 | ], 26 | "max-line-length": [ 27 | true, 28 | 150 29 | ], 30 | "no-angle-bracket-type-assertion": true, 31 | "no-consecutive-blank-lines": [ 32 | true, 33 | 2 34 | ], 35 | "no-console": false, 36 | "no-floating-promises": true, 37 | "no-trailing-whitespace": true, 38 | "prefer-for-of-loop": false, 39 | "no-default-export": true, 40 | "no-duplicate-variable": true, 41 | "no-bitwise": false, 42 | "no-var-keyword": true, 43 | "no-var-requires": false, 44 | "no-empty": true, 45 | "no-empty-interface": true, 46 | "no-unused-expression": false, 47 | "no-reference": true, 48 | "no-use-before-declare": true, 49 | "no-submodule-imports": [ 50 | false 51 | ], 52 | "no-implicit-dependencies": [ 53 | false 54 | ], 55 | "no-this-assignment": [ 56 | true, 57 | { 58 | "allow-destructuring": true 59 | } 60 | ], 61 | "interface-name": [ 62 | false 63 | ], 64 | "object-literal-sort-keys": false, 65 | "object-literal-key-quotes": [ 66 | true, 67 | "as-needed" 68 | ], 69 | "only-arrow-functions": [ 70 | false 71 | ], 72 | "ordered-imports": [ 73 | true, 74 | { 75 | "import-sources-order": "lowercase-last", 76 | "named-imports-order": "lowercase-last" 77 | } 78 | ], 79 | "one-variable-per-declaration": [ 80 | false 81 | ], 82 | "one-line": [ 83 | false, 84 | "check-else", 85 | "check-whitespace", 86 | "check-open-brace" 87 | ], 88 | "prefer-const": true, 89 | "prefer-conditional-expression": false, 90 | "quotemark": [ 91 | true, 92 | "single", 93 | "avoid-escape" 94 | ], 95 | "max-classes-per-file": false, 96 | "semicolon": [ 97 | true, 98 | "always" 99 | ], 100 | "switch-default": false, 101 | "trailing-comma": [ 102 | false 103 | ], 104 | "typedef": [ 105 | true, 106 | "parameter", 107 | "property-declaration" 108 | ], 109 | "typedef-whitespace": [ 110 | true, 111 | { 112 | "call-signature": "nospace", 113 | "index-signature": "nospace", 114 | "parameter": "nospace", 115 | "property-declaration": "nospace", 116 | "variable-declaration": "nospace" 117 | } 118 | ], 119 | "variable-name": [ 120 | true, 121 | "allow-leading-underscore", 122 | "ban-keywords" 123 | ], 124 | "whitespace": [ 125 | true, 126 | "check-branch", 127 | "check-decl", 128 | "check-operator", 129 | "check-separator", 130 | "check-type" 131 | ] 132 | }, 133 | "rulesDirectory": [ 134 | "node_modules/tslint-microsoft-contrib" 135 | ] 136 | } 137 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = (wallaby) => ({ 2 | files: [ 3 | 'src/**/*.js?(x)', 4 | 'src/**/*.ts?(x)', 5 | 'src/**/*.html', 6 | { pattern: '__tests__/**/*-test.ts?(x)', ignore: true }, 7 | { pattern: '__tests__/**/!(*-test)', instrument: false, load: true }, 8 | { pattern: 'package.json', instrument: false, load: true } 9 | ], 10 | 11 | tests: [ 12 | '__tests__/**/*-test.ts?(x)' 13 | ], 14 | 15 | env: { 16 | type: 'node', 17 | runner: 'node', 18 | params: { env: 'wallaby=true' } 19 | }, 20 | 21 | testFramework: 'jest', 22 | 23 | // Enable mock hoisting as same as ts-jest does 24 | // (https://github.com/kulshekhar/ts-jest#supports-automatic-of-jestmock-calls) 25 | preprocessors: { 26 | '**/*.js?(x)': (file) => require('babel-core').transform( 27 | file.content, 28 | { sourceMaps: true, filename: file.path, presets: ['babel-preset-jest'] }) 29 | }, 30 | 31 | workers: { 32 | initial: 2, 33 | regular: 1 34 | }, 35 | 36 | setup: (w) => { 37 | const path = require('path'); 38 | 39 | const jestConfig = { 40 | resetMocks: true, 41 | resetModules: true, 42 | moduleFileExtensions: [ 43 | 'js', 44 | 'jsx', 45 | 'json', 46 | 'ts', 47 | 'tsx' 48 | ], 49 | globals: { __JEST_DEV__: true } 50 | }; 51 | 52 | w.testFramework.configure(jestConfig); 53 | } 54 | }); 55 | --------------------------------------------------------------------------------