diff --git a/packages/doctor/.github/workflows/npm_release.yml b/packages/doctor/.github/workflows/npm_release.yml new file mode 100644 index 0000000000..9b0c0a5ea5 --- /dev/null +++ b/packages/doctor/.github/workflows/npm_release.yml @@ -0,0 +1,36 @@ +name: '@nativescript/doctor -> npm' + +on: + push: + branches: [ 'master' ] + +env: + NPM_TAG: 'next' + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Setup + run: npm install + + - name: Generate Version + run: | + echo NPM_VERSION=$(node -e "console.log(require('./package.json').version);")-$NPM_TAG-$(date +"%m-%d-%Y")-$GITHUB_RUN_ID >> $GITHUB_ENV + + - name: Bump Version + run: npm --no-git-tag-version version $NPM_VERSION + + - name: Build @nativescript/doctor + run: npm run pack + + - name: Publish @nativescript/doctor + env: + NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + run: | + echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc + echo "Publishing @nativescript/doctor@$NPM_VERSION to NPM with tag $NPM_TAG..." + npm publish nativescript-doctor-$NPM_VERSION.tgz --tag $NPM_TAG diff --git a/packages/doctor/.gitignore b/packages/doctor/.gitignore new file mode 100644 index 0000000000..bc592ea82e --- /dev/null +++ b/packages/doctor/.gitignore @@ -0,0 +1,69 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages +package-lock.json + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +npm-debug.log +node_modules +docs/html +tscommand*.tmp.txt +.tscache/ +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz +*.tgz +*.tmp +*.sublime-workspace + +pids +logs +results +scratch/ +.idea/ +.settings/ +.vscode/ +test-reports.xml + +*.js +*.js.map +/lib/.d.ts +.d.ts +!/*.js diff --git a/packages/doctor/.npmignore b/packages/doctor/.npmignore new file mode 100644 index 0000000000..cdd6ddc90c --- /dev/null +++ b/packages/doctor/.npmignore @@ -0,0 +1,35 @@ +.idea +.gitattributes +.gitmodules +*.sublime-project +lint.* +.jshint* +.npmignore +*.tgz +test-reports.xml +for-developers.md +prepublish.js +Gruntfile.js +BuildPackage.cmd +tscommand.tmp.txt +.tscache/ + +bin/nativescript +bin/*.cmd + +lib/**/*.ts +lib/**/*.js.map + +test/ +.vscode +coverage/ +scratch/ +*.suo +.travis.yml +docs/html/ +dev/ + +tscommand*.tmp.txt + +tslint.json +tsconfig.json diff --git a/packages/doctor/.travis.yml b/packages/doctor/.travis.yml new file mode 100644 index 0000000000..7494adcd45 --- /dev/null +++ b/packages/doctor/.travis.yml @@ -0,0 +1,11 @@ +language: node_js +node_js: +- '14' +git: + submodules: false +before_script: +- npm install grunt-cli +- npm install +script: +- node_modules/.bin/grunt lint +- node_modules/.bin/grunt pack --no-color diff --git a/packages/doctor/CHANGELOG.md b/packages/doctor/CHANGELOG.md new file mode 100644 index 0000000000..0c404e1b54 --- /dev/null +++ b/packages/doctor/CHANGELOG.md @@ -0,0 +1,47 @@ +## [2.0.5](https://github.com/NativeScript/nativescript-doctor/compare/v2.0.4...v2.0.5) (2021-10-08) + + +### Bug Fixes + +* android-31 target version ([#71](https://github.com/NativeScript/nativescript-doctor/issues/71)) ([b7c7278](https://github.com/NativeScript/nativescript-doctor/commit/b7c7278b1425950a3168784293090617c021303f)) +* removed max java version limitation ([#67](https://github.com/NativeScript/nativescript-doctor/issues/67)) ([3e96ec1](https://github.com/NativeScript/nativescript-doctor/commit/3e96ec16298f5205ed2511baf94b23d36d80c454)) +* **windows:** handle paths correctly ([#64](https://github.com/NativeScript/nativescript-doctor/issues/64)) ([9db60b6](https://github.com/NativeScript/nativescript-doctor/commit/9db60b64970d229c539e03b888b1e93195367da4)) + + + +## [2.0.4](https://github.com/NativeScript/nativescript-doctor/compare/v2.0.3...v2.0.4) (2020-09-06) + + +### Bug Fixes + +* runtime version validation and getMaxSupportedCompileVersion ([a7d71a0](https://github.com/NativeScript/nativescript-doctor/commit/a7d71a0dde6e174d507098dad388ba7d6db3163b)) + + + +## [2.0.3](https://github.com/NativeScript/nativescript-doctor/compare/v2.0.1...v2.0.3) (2020-09-06) + + +### Bug Fixes + +* support tgz runtime versions ([20641e7](https://github.com/NativeScript/nativescript-doctor/commit/20641e7528c189161138822504389436ce6805ce)) + + + +## [2.0.1](https://github.com/NativeScript/nativescript-doctor/compare/v1.14.2...v2.0.1) (2020-09-06) + + +### Features + +* nativescript 7 support ([6b88f4c](https://github.com/NativeScript/nativescript-doctor/commit/6b88f4c2195cf15e20e9df043270b1cf105093fe)) + + + +## [1.14.2](https://github.com/NativeScript/nativescript-doctor/compare/v1.14.1...v1.14.2) (2020-06-25) + + +### Features + +* **android:** support v30 ([#61](https://github.com/NativeScript/nativescript-doctor/issues/61)) ([20612a2](https://github.com/NativeScript/nativescript-doctor/commit/20612a2b36bbe684dfe7b9c7191399c8bade9773)) + + + diff --git a/packages/doctor/Gruntfile.js b/packages/doctor/Gruntfile.js new file mode 100644 index 0000000000..252b427b3a --- /dev/null +++ b/packages/doctor/Gruntfile.js @@ -0,0 +1,113 @@ +module.exports = function (grunt) { + grunt.initConfig({ + ts: { + options: grunt.file.readJSON("tsconfig.json").compilerOptions, + + devlib: { + src: ["lib/**/*.ts", "typings/**/*.ts"], + reference: "lib/.d.ts" + }, + + devall: { + src: ["lib/**/*.ts", "test/**/*.ts", "typings/**/*.ts"], + reference: "lib/.d.ts" + }, + + release_build: { + src: ["lib/**/*.ts", "test/**/*.ts", "typings/**/*.ts"], + reference: "lib/.d.ts", + options: { + sourceMap: false, + removeComments: true + } + } + }, + + tslint: { + build: { + files: { + src: ["lib/**/*.ts", "test/**/*.ts", "typings/**/*.ts", "!**/*.d.ts"] + }, + options: { + configuration: grunt.file.readJSON("./tslint.json"), + project: "tsconfig.json" + } + } + }, + + watch: { + devall: { + files: ["lib/**/*.ts", 'test/**/*.ts'], + tasks: [ + 'ts:devall', + 'shell:npm_test' + ], + options: { + atBegin: true, + interrupt: true + } + }, + ts: { + files: ["lib/**/*.ts", "test/**/*.ts"], + tasks: [ + 'ts:devall' + ], + options: { + atBegin: true, + interrupt: true + } + } + }, + + shell: { + options: { + stdout: true, + stderr: true, + failOnError: true + }, + build_package: { + command: "npm pack" + }, + npm_test: { + command: "npm test" + } + }, + + clean: { + src: ["test/**/*.js*", "lib/**/*.js*", "!lib/hooks/**/*.js", "!Gruntfile.js", "*.tgz"] + } + }); + + grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks("grunt-contrib-watch"); + grunt.loadNpmTasks("grunt-shell"); + grunt.loadNpmTasks("grunt-ts"); + grunt.loadNpmTasks("grunt-tslint"); + + grunt.registerTask("delete_coverage_dir", function () { + var done = this.async(); + var rimraf = require("rimraf"); + rimraf("coverage", function (err) { + if (err) { + console.log("Error while deleting coverage directory from the package: ", err); + done(false); + } + + done(); + }); + }); + + grunt.registerTask("test", ["ts:devall", "shell:npm_test"]); + + grunt.registerTask("pack", [ + "clean", + "ts:release_build", + "shell:npm_test", + "delete_coverage_dir", + "shell:build_package" + ]); + grunt.registerTask("lint", ["tslint:build"]); + grunt.registerTask("all", ["clean", "test", "lint"]); + grunt.registerTask("rebuild", ["clean", "ts:devlib"]); + grunt.registerTask("default", "ts:devlib"); +}; diff --git a/packages/doctor/LICENSE b/packages/doctor/LICENSE new file mode 100755 index 0000000000..061c440288 --- /dev/null +++ b/packages/doctor/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2019 Progress Software Corporation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/doctor/NOTICE.txt b/packages/doctor/NOTICE.txt new file mode 100644 index 0000000000..3e0fec49de --- /dev/null +++ b/packages/doctor/NOTICE.txt @@ -0,0 +1,88 @@ +========================================================================= +== NOTICE file corresponding to section 4 d of == +== the Apache License, Version 2.0, == +== in this case for NativeScript Doctor v1* == +========================================================================= + +NativeScript Doctor v1* (the “Product”) + +Copyright © 2016-2018 Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. + +For license information see the LICENSE.md file which accompanies this NOTICE.txt file. + +Portions of the Product include certain open source and commercial third-party components listed below (ìThird-Party Componentsî). The authors of the Third-Party Components require Progress Software Corporation (ìPSCî) to include the following notices and additional licensing terms as a condition of PSCís use of such Third-Party Components. You acknowledge that the authors of the Third-Party Components have no obligation to provide support to you for the Third-Party Components or the Product. You hereby undertake to comply with all licenses related to the applicable Third-Party Components. Notwithstanding anything to the contrary, to the extent that any of the terms and conditions of the Product Agreement conflict, vary, or are in addition to the terms and conditions of the aforementioned third-party licenses for these technology, such terms and conditions are offered by PSC alone and not by any other party. + +1. Special Notices Regarding Open Source Third-Party Components incorporated in the Product: + +(1) BSD-Style License: + +(a) NativeScript Doctor v1* incorporates winreg v1.2.2. Such technology is subject to the following terms and conditions: +Copyright (c) 2016, Paul Bottin All rights reserved. +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +(2) ISC-Style License: + +(a) NativeScript Doctor v1* incorporates semver v5.3. Such technology is subject to the following terms and conditions: +The ISC License +Copyright (c) Isaac Z. Schlueter and Contributors +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +(b) NativeScript Doctor v1* incorporates osenv v0.1.3. Such technology is subject to the following terms and conditions: +The ISC License +Copyright (c) Isaac Z. Schlueter and Contributors +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +(3) MIT-Style License: + +(a) NativeScript Doctor v1* incorporates temp v0.8.3. Such technology is subject to the following terms and conditions: +The MIT License (MIT) +Copyright (c) 2010-2014 Bruce Williams +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +(b) NativeScript Doctor v1* incorporates node-unzip v1.11. Such technology is subject to the following terms and conditions: +Copyright (c) 2012 - 2013 Near Infinity Corporation +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +2. Special Notices Regarding Commercially Licensed Third-Party Components incorporated in the Product: None + + +NOTICE FROM PROGRESS SOFTWARE CORPORATION: Additional notices may be included in the release notes or other documentation that accompanies updates received in connection with support of the Product. + +11/20/2018 diff --git a/packages/doctor/README.md b/packages/doctor/README.md new file mode 100644 index 0000000000..28237a8c5a --- /dev/null +++ b/packages/doctor/README.md @@ -0,0 +1,566 @@ +# @nativescript/doctor +Library that helps identifying if the environment can be used for development of {N} apps. + +# Installation +1. Using npm + ```bash + $ npm install @nativescript/doctor --save + ``` + +# Requirements +1. Node.js 4.3.0 or later + +# Usage +* Module `doctor`: + - Usage: + ```TypeScript + import { doctor } from "@nativescript/doctor" + + async function main() { + const canExecuteLocalBuildForAndroid = await doctor.canExecuteLocalBuild("Android"); + const canExecuteLocalBuildForIos = await doctor.canExecuteLocalBuild("iOS"); + console.log("Can execute local build for Android: ", canExecuteLocalBuildForAndroid); + console.log("Can execute local build for iOS: ", canExecuteLocalBuildForIos); + } + + main(); + ``` + + - Interfaces: + ```TypeScript + /** + * Describes methods which help identifying if the environment can be used for development of {N} apps. + */ + interface IDoctor { + /** + * Checks if a local build can be executed on the current machine. + * @param {string} platform The platform for which to check if local build is possible. + * @return {Promise} true if local build can be executed for the provided platform. + */ + canExecuteLocalBuild(platform: string): Promise; + + /** + * Executes all checks for the current environment and returns the warnings from each check. + * @return {Promise} Array of all the warnings from all checks. If there are no warnings will return empty array. + */ + getWarnings(): Promise; + } + + /** + * Describes warning returned from @nativescript/doctor check. + */ + interface IWarning { + /** + * The warning. + * @type {string} + */ + warning: string; + + /** + * Additional information for the warning. + * @type {string} + */ + additionalInformation: string; + + /** + * The platforms which are affected by this warning. + * @type {string[]} + */ + platforms: string[]; + } + ``` + +* Module `sysInfo`: + - Usage: + ```TypeScript + import { sysInfo, setShouldCacheSysInfo } from "@nativescript/doctor"; + + async function main() { + // The default value is true. If set to false the result of each check for each element + // of the sys info will not be cached. + setShouldCacheSysInfo(false); + + const javacVersion = await sysInfo.getJavaCompilerVersion(); + console.log("javac: ", javacVersion); + + const adbVersion = await sysInfo.getAdbVersion(); + console.log("adb: ", adbVersion); + + const cocoaPodsVersion = await sysInfo.getCocoaPodsVersion(); + console.log("cocoapods: ", cocoaPodsVersion); + + const gitVersion = await sysInfo.getGitVersion(); + console.log("git: ", gitVersion); + + const gradleVersion = await sysInfo.getGradleVersion(); + console.log("gradle: ", gradleVersion); + + const monoVersion = await sysInfo.getMonoVersion(); + console.log("mono: ", monoVersion); + + const nodeVersion = await sysInfo.getNodeVersion(); + console.log("node: ", nodeVersion); + + const npmVersion = await sysInfo.getNpmVersion(); + console.log("npm: ", npmVersion); + + const nodeGypVersion = await sysInfo.getNodeGypVersion(); + console.log("node-gyp: ", nodeGypVersion); + + const osName = await sysInfo.getOs(); + console.log("os: ", osName); + + const xcodeprojLocation = await sysInfo.getXCodeProjLocation(); + console.log("xcodeproj location: ", xcodeprojLocation); + + const xcodeVersion = await sysInfo.getXCodeVersion(); + console.log("xcode: ", xcodeVersion); + + const isAndroidInstalled = await sysInfo.isAndroidInstalled(); + console.log("is Android installed: ", isAndroidInstalled); + + const isITunesInstalled = await sysInfo.isITunesInstalled(); + console.log("is iTunes installed: ", isITunesInstalled); + + const isCocoaPodsWorkingCorrectly = await sysInfo.isCocoaPodsWorkingCorrectly(); + console.log("is cocoapods working correctly: ", isCocoaPodsWorkingCorrectly); + + const nativeScriptCliVersion = await sysInfo.getNativeScriptCliVersion(); + console.log("{N} CLI version: ", nativeScriptCliVersion); + + const xcprojInfo = await sysInfo.getXcprojInfo(); + console.log("xcproj info: ", xcprojInfo); + + const isCocoaPodsUpdateRequired = await sysInfo.isCocoaPodsUpdateRequired(); + console.log("is CocoaPods update required: ", isCocoaPodsUpdateRequired); + + const pythonInfo = await sysInfo.getPythonInfo(); + console.log("python info: ", pythonInfo ); + + const sysInfoData = await sysInfo.getSysInfo({ projectDir: "/Users/username/myProject" }); + console.log("sysInfo: ", sysInfoData); + + const gitPath = await sysInfo.getGitPath(); + console.log("Path to the git executable: ", gitPath); + } + + main(); + + ``` + + - Interfaces: + ```TypeScript + /** + * Describes methods which helps collecting system information. + */ + interface ISysInfo { + /** + * Returns the currently installed Java compiler version. + * @return {Promise} The currently installed Java compiler version. + */ + getJavaCompilerVersion(): Promise; + + /** + * Returns the currently installed version of Xcode. + * @return {Promise} Returns the currently installed version of Xcode or null if Xcode is not installed or executed on Linux or Windows. + */ + getXcodeVersion(): Promise; + + /** + * Returns the currently installed Node.js version. + * @return {Promise} Returns the currently installed Node.js version. + */ + getNodeVersion(): Promise; + + /** + * Returns the currently installed npm version. + * @return {Promise} Returns the currently installed npm version. + */ + getNpmVersion(): Promise; + + /** + * Returns the currently installed node-gyp version. + * @return {Promise} Returns the currently installed node-gyp version. If node-gyp is not installed it will return null. + */ + getNodeGypVersion(): Promise; + + /** + * Returns the xcodeproj location. + * @return {Promise} Returns the xcodeproj location. If the the xcodeproj is not installed it will return null. + */ + getXcodeprojLocation(): Promise; + + /** + * Checks if iTunes is installed. + * @return {Promise} Returns true if iTunes is installed. + */ + isITunesInstalled(): Promise; + + /** + * Returns the currently installed Cocoapods version. + * @return {Promise} Returns the currently installed Cocoapods version. It will return null if Cocoapods is not installed. + */ + getCocoaPodsVersion(): Promise; + + /** + * Returns the os name. + * @return {Promise} Returns the os name. + */ + getOs(): Promise; + + /** + * Returns the currently installed ADB version. + * @param {string} pathToAdb Defines path to adb + * @return {Promise} Returns the currently installed ADB version. It will return null if ADB is not installed. + */ + getAdbVersion(pathToAdb?: string): Promise; + + /** + * Checks if Android is installed. + * @return {Promise} Returns true if Android is installed. + */ + isAndroidInstalled(): Promise; + + /** + * Returns the currently installed Mono version. + * @return {Promise} Returns the currently installed Mono version. It will return null if Mono is not installed. + */ + getMonoVersion(): Promise; + + /** + * Returns the currently installed Git version. + * @return {Promise} Returns the currently installed Git version. It will return null if Git is not installed. + */ + getGitVersion(): Promise; + + /** + * Returns the currently installed Gradle version. + * @return {Promise} Returns the currently installed Gradle version. It will return null if Gradle is not installed. + */ + getGradleVersion(): Promise; + + /** + * Checks if CocoaPods is working correctly by trying to install one pod. + * @return {Promise} Returns true if CocoaPods is working correctly. + */ + isCocoaPodsWorkingCorrectly(): Promise; + + /** + * Returns the version of the globally installed NativeScript CLI. + * @return {Promise} Returns the version of the globally installed NativeScript CLI. + */ + getNativeScriptCliVersion(): Promise; + + /** + * Checks if xcproj is required to build projects and if it is installed. + * @return {Promise} Returns the collected information aboud xcproj. + */ + getXcprojInfo(): Promise; + + /** + * Checks if the current version of CocoaPods is compatible with the installed Xcode. + * @return {boolean} true if an update us require. + */ + isCocoaPodsUpdateRequired(): Promise; + + /** + * Checks if the Android SDK Tools are installed and configured correctly. + * @return {Promise} true if the Android SDK Tools are installed and configured correctly. + */ + isAndroidSdkConfiguredCorrectly(): Promise; + + /** + * Returns the whole system information. + * @param {ISysInfoConfig} config + * @return {Promise} The system information. + */ + getSysInfo(config?: ISysInfoConfig): Promise; + + /** + * If set to true each method will cache it's result. The default value is true. + * @param {boolean} shouldCache The cache switch. + * @return {void} + */ + setShouldCacheSysInfo(shouldCache: boolean): void; + + /** + * Returns the path to the currently installed Git. + * @return {Promise} Returns the path to the currently installed Git. It will return null if Git is not installed. + */ + getGitPath(): Promise; + } + + interface ISysInfoData { + // os stuff + /** + * Os platform flavour, reported by os.platform. + * @type {string} + */ + platform: string; + + /** + * Full os name, like `uname -a` on unix, registry query on win. + * @type {string} + */ + os: string; + + /** + * .net version, applicable to windows only. + * @type {string} + */ + dotNetVer: string; + + /** + * The command shell in use, usually bash or cmd. + * @type {string} + */ + shell: string; + + // node stuff + /** + * node.js version, returned by node -v. + * @type {string} + */ + nodeVer: string; + + /** + * npm version, returned by `npm -v`. + * @type {string} + */ + npmVer: string; + + /** + * Process architecture, returned by `process.arch`. + * @type {string} + */ + procArch: string; + + /** + * node-gyp version as returned by `node-gyp -v`. + * @type {string} + */ + nodeGypVer: string; + + // dependencies + /** + * Xcode version string as returned by `xcodebuild -version`. Valid only on Mac. + * @type {string} + */ + xcodeVer: string; + + /** + * Version string of adb, as returned by `adb version`. + * @type {string} + */ + adbVer: string; + + /** + * Whether iTunes is installed on the machine. + * @type {boolean} + */ + itunesInstalled: boolean; + + /** + * Whether `android` executable can be run. + * @type {boolean} + */ + androidInstalled: boolean; + + /** + * mono version, relevant on Mac only. + * @type {string} + */ + monoVer: string; + + /** + * git version string, as returned by `git --version`. + * @type {string} + */ + gitVer: string; + + /** + * gradle version string as returned by `gradle -v`. + * @type {string} + */ + gradleVer: string; + + /** + * javac version string as returned by `javac -version`. + * @type {string} + */ + javacVersion: string; + + /** + * pod version string, as returned by `pod --version`. + * @type {string} + */ + cocoaPodsVer: string; + + /** + * xcodeproj gem location, as returned by `which gem xcodeproj`. + * @type {string} + */ + xcodeprojLocation: string; + + /** + * true id CocoaPods can successfully execute pod install. + * @type {boolean} + */ + isCocoaPodsWorkingCorrectly: boolean; + + /** + * NativeScript CLI version string, as returned by `tns --version`. + * @type {string} + */ + nativeScriptCliVersion: string; + + /** + * Information about xcproj. + * @type {string} + */ + xcprojInfo: IXcprojInfo; + + /** + * true if the system requires xcproj to build projects successfully and the CocoaPods version is not compatible with the Xcode. + */ + isCocoaPodsUpdateRequired: boolean; + + /** + * true if the Android SDK Tools are installed and configured correctly. + * @type {boolean} + */ + isAndroidSdkConfiguredCorrectly: boolean; + } + + /** + * Describes information about xcproj brew formula. + */ + interface IXcprojInfo { + /** + * Determines whether the system needs xcproj to execute ios builds sucessfully. + */ + shouldUseXcproj: boolean; + + /** + * Determines whether xcproj can be called from the command line. + */ + xcprojAvailable: boolean; + } + ``` + +* Module `androidToolsInfo`: + - Usage: + ```TypeScript + import { androidToolsInfo } from "@nativescript/doctor" + + function main() { + const projectDir = "/Users/username/myProject"; + console.log("path to adb from android home: ", await androidToolsInfo.getPathToAdbFromAndroidHome()); + console.log("path to emulator executable: ", androidToolsInfo.getPathToEmulatorExecutable()); + console.log("android tools info: ", androidToolsInfo.getToolsInfo()); + console.log("ANROID_HOME validation errors: ", await androidToolsInfo.validateAndroidHomeEnvVariable()); + console.log("android tools info validation errors: ", await androidToolsInfo.validateInfo()); + console.log("javac validation errors: ", await androidToolsInfo.validateJavacVersion(await sysInfo.getJavaCompilerVersion(), projectDir)); + } + + main(); + ``` + - Interfaces: + ```TypeScript + /** + * Describes methods for getting and validating Android tools. + */ + interface IAndroidToolsInfo { + /** + * Returns the Android tools info. + * @return {NativeScriptDoctor.IAndroidToolsInfoData} returns the Android tools info. + */ + getToolsInfo(): NativeScriptDoctor.IAndroidToolsInfoData; + + /** + * Checks if the Android tools are valid. + * @param {string} projectDir @optional The project directory. Used to determine the Android Runtime version and validate the Java compiler version against it. + * If it is not passed or the project does not have Android runtime, this validation is skipped. + * @return {NativeScriptDoctor.IWarning[]} An array of errors from the validation checks. If there are no errors will return []. + */ + validateInfo(projectDir?: string): NativeScriptDoctor.IWarning[]; + + /** + * Checks if the current javac version is valid. + * @param {string} installedJavaVersion The version of javac to check. + * @param {string} projectDir @optional The project directory. Used to determine the Android Runtime version and validate the Java compiler version against it. + * If it is not passed or the project does not have Android runtime, this validation is skipped. + * @return {NativeScriptDoctor.IWarning[]} An array of errors from the validation checks. If there are no errors will return []. + */ + validateJavacVersion(installedJavaVersion: string, projectDir?: string): NativeScriptDoctor.IWarning[]; + + /** + * Returns the path to the adb which is located in ANDROID_HOME. + * @return {Promise} Path to the adb which is located in ANDROID_HOME. + */ + getPathToAdbFromAndroidHome(): Promise; + + /** + * Checks if the ANDROID_HOME variable is set to the correct folder. + * @return {NativeScriptDoctor.IWarning[]} An array of errors from the validation checks. If there are no errors will return []. + */ + validateAndroidHomeEnvVariable(): NativeScriptDoctor.IWarning[]; + + /** + * Returns the path to the emulator executable. + * @return {string} The path to the emulator executable. + */ + getPathToEmulatorExecutable(): string; + } + + /** + * Describes information about installed Android tools and SDKs. + */ + interface IAndroidToolsInfoData { + /** + * The value of ANDROID_HOME environment variable. + */ + androidHomeEnvVar: string; + + /** + * The latest installed version of Android Build Tools that satisfies CLI's requirements. + */ + buildToolsVersion: string; + + /** + * The latest installed version of Android SDK that satisfies CLI's requirements. + */ + compileSdkVersion: number; + + /** + * The latest installed version of Android Support Repository that satisfies CLI's requirements. + */ + supportRepositoryVersion: string; + } + ``` + +* Module `constants`: + - Usage: + ```TypeScript + import { constants } from "@nativescript/doctor" + + function main() { + for(let constantName in constants) { + console.log(`${constantName}: ${constants[constantName]}`); + } + } + + main(); + ``` + + - Interfaces: + ```TypeScript + /** + * Describes the constants used in the module. + */ + interface IConstants { + ANDROID_PLATFORM_NAME: string; + IOS_PLATFORM_NAME: string; + SUPPORTED_PLATFORMS: string[]; + } + ``` diff --git a/packages/doctor/lib/android-tools-info.ts b/packages/doctor/lib/android-tools-info.ts new file mode 100644 index 0000000000..9430076e6a --- /dev/null +++ b/packages/doctor/lib/android-tools-info.ts @@ -0,0 +1,475 @@ +import { ChildProcess } from "./wrappers/child-process"; +import { FileSystem } from "./wrappers/file-system"; +import { HostInfo } from "./host-info"; +import { Constants } from "./constants"; +import { Helpers } from './helpers'; +import { EOL } from "os"; +import * as semver from "semver"; +import * as path from "path"; +import * as _ from "lodash"; + +export class AndroidToolsInfo implements NativeScriptDoctor.IAndroidToolsInfo { + public readonly ANDROID_TARGET_PREFIX = "android"; + public getSupportedTargets(projectDir: string) { + const runtimeVersion = this.getRuntimeVersion({ projectDir }); + let baseTargets = [ + "android-17", + "android-18", + "android-19", + "android-20", + "android-21", + "android-22", + "android-23", + "android-24", + "android-25", + "android-26", + "android-27", + "android-28", + "android-29", + "android-30", + "android-31", + ]; + + if (runtimeVersion && semver.lt(semver.coerce(runtimeVersion), "6.1.0")) { + baseTargets.sort(); + const indexOfSdk29 = baseTargets.indexOf("android-29"); + baseTargets = baseTargets.slice(0, indexOfSdk29); + } + + return baseTargets; + } + private static MIN_REQUIRED_COMPILE_TARGET = 28; + private static REQUIRED_BUILD_TOOLS_RANGE_PREFIX = ">=23"; + private static VERSION_REGEX = /((\d+\.){2}\d+)/; + private static MIN_JAVA_VERSION = "1.8.0"; + // If some java release breaks the code then set this version to the breaking release (e.g. "13.0.0") + private static MAX_JAVA_VERSION = null as string; + + private toolsInfo: NativeScriptDoctor.IAndroidToolsInfoData; + public get androidHome(): string { + return process.env["ANDROID_HOME"]; + } + private pathToEmulatorExecutable: string; + + constructor(private childProcess: ChildProcess, + private fs: FileSystem, + private hostInfo: HostInfo, + private helpers: Helpers) { } + + public getToolsInfo(config: Partial = {}): NativeScriptDoctor.IAndroidToolsInfoData { + if (!this.toolsInfo) { + const infoData: NativeScriptDoctor.IAndroidToolsInfoData = Object.create(null); + infoData.androidHomeEnvVar = this.androidHome; + infoData.installedTargets = this.getInstalledTargets(); + infoData.compileSdkVersion = this.getCompileSdk(infoData.installedTargets, config.projectDir); + infoData.buildToolsVersion = this.getBuildToolsVersion(config.projectDir); + + this.toolsInfo = infoData; + } + + return this.toolsInfo; + } + + public validateInfo(config: Partial = {}): NativeScriptDoctor.IWarning[] { + const errors: NativeScriptDoctor.IWarning[] = []; + const toolsInfoData = this.getToolsInfo(config); + const isAndroidHomeValid = this.isAndroidHomeValid(); + const supportsOnlyMinRequiredCompileTarget = this.getMaxSupportedCompileVersion(config) === AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET; + + if (!toolsInfoData.compileSdkVersion) { + errors.push({ + warning: `Cannot find a compatible Android SDK for compilation. To be able to build for Android, install Android SDK ${AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET}${supportsOnlyMinRequiredCompileTarget ? "" : " or later"}.`, + additionalInformation: `Run \`\$ ${this.getPathToSdkManagementTool()}\` to manage your Android SDK versions.`, + platforms: [Constants.ANDROID_PLATFORM_NAME] + }); + } + + if (!toolsInfoData.buildToolsVersion) { + const buildToolsRange = this.getBuildToolsRange(config.projectDir); + const versionRangeMatches = buildToolsRange.match(/^.*?([\d\.]+)\s+.*?([\d\.]+)$/); + let message = `You can install any version in the following range: '${buildToolsRange}'.`; + + // Improve message in case buildToolsRange is something like: ">=22.0.0 <=22.0.0" - same numbers on both sides + if (versionRangeMatches && versionRangeMatches[1] && versionRangeMatches[2] && versionRangeMatches[1] === versionRangeMatches[2]) { + message = `You have to install version ${versionRangeMatches[1]}.`; + } + + let invalidBuildToolsAdditionalMsg = `Run \`\$ ${this.getPathToSdkManagementTool()}\` from your command-line to install required \`Android Build Tools\`.`; + if (!isAndroidHomeValid) { + invalidBuildToolsAdditionalMsg += ' In case you already have them installed, make sure `ANDROID_HOME` environment variable is set correctly.'; + } + + errors.push({ + warning: "You need to have the Android SDK Build-tools installed on your system. " + message, + additionalInformation: invalidBuildToolsAdditionalMsg, + platforms: [Constants.ANDROID_PLATFORM_NAME] + }); + } + + return errors; + } + + public static unsupportedJavaMessage(installedJavaCompilerVersion: string): string { + return `Javac version ${installedJavaCompilerVersion} is not supported. You must install a java version greater than ${ + AndroidToolsInfo.MIN_JAVA_VERSION + }${ + AndroidToolsInfo.MAX_JAVA_VERSION + ? ` and less than ${AndroidToolsInfo.MAX_JAVA_VERSION}` + : "" + }.`; + } + + public validateJavacVersion(installedJavaCompilerVersion: string, projectDir?: string, runtimeVersion?: string): NativeScriptDoctor.IWarning[] { + const errors: NativeScriptDoctor.IWarning[] = []; + + let additionalMessage = "You will not be able to build your projects for Android." + EOL + + "To be able to build for Android, verify that you have installed The Java Development Kit (JDK) and configured it according to system requirements as" + EOL + + " described in " + this.getSystemRequirementsLink(); + + const matchingVersion = this.helpers.appendZeroesToVersion(installedJavaCompilerVersion || "", 3).match(AndroidToolsInfo.VERSION_REGEX); + const installedJavaCompilerSemverVersion = matchingVersion && matchingVersion[1]; + if (installedJavaCompilerSemverVersion) { + let warning: string = null; + + const supportedVersions: IDictionary = { + "^10.0.0": "4.1.0-2018.5.18.1" + }; + + if (semver.lt(installedJavaCompilerSemverVersion, AndroidToolsInfo.MIN_JAVA_VERSION) || (AndroidToolsInfo.MAX_JAVA_VERSION ? semver.gte(installedJavaCompilerSemverVersion, AndroidToolsInfo.MAX_JAVA_VERSION) : false)) { + warning = AndroidToolsInfo.unsupportedJavaMessage(installedJavaCompilerVersion); + } else { + runtimeVersion = this.getRuntimeVersion({ runtimeVersion, projectDir }); + if (runtimeVersion) { + // get the item from the dictionary that corresponds to our current Javac version: + let runtimeMinVersion: string = null; + Object.keys(supportedVersions) + .forEach(javacRange => { + if (semver.satisfies(installedJavaCompilerSemverVersion, javacRange)) { + runtimeMinVersion = supportedVersions[javacRange]; + } + }); + + if (runtimeMinVersion && semver.lt(runtimeVersion, runtimeMinVersion)) { + warning = `The Java compiler version ${installedJavaCompilerVersion} is not compatible with the current Android runtime version ${runtimeVersion}. ` + + `In order to use this Javac version, you need to update your Android runtime or downgrade your Java compiler version.`; + additionalMessage = "You will not be able to build your projects for Android." + EOL + + "To be able to build for Android, downgrade your Java compiler version or update your Android runtime."; + } + } + } + + if (warning) { + errors.push({ + warning, + additionalInformation: additionalMessage, + platforms: [Constants.ANDROID_PLATFORM_NAME] + }); + } + } else { + errors.push({ + warning: "Error executing command 'javac'. Make sure you have installed The Java Development Kit (JDK) and set JAVA_HOME environment variable.", + additionalInformation: additionalMessage, + platforms: [Constants.ANDROID_PLATFORM_NAME] + }); + } + + return errors; + } + + public async getPathToAdbFromAndroidHome(): Promise { + if (this.androidHome) { + const pathToAdb = path.join(this.androidHome, "platform-tools", "adb"); + try { + await this.childProcess.execFile(pathToAdb, ["help"]); + return pathToAdb; + } catch (err) { + return null; + } + } + + return null; + } + + public validateAndroidHomeEnvVariable(): NativeScriptDoctor.IWarning[] { + const errors: NativeScriptDoctor.IWarning[] = []; + const expectedDirectoriesInAndroidHome = ["build-tools", "tools", "platform-tools", "extras"]; + + if (!this.androidHome || !this.fs.exists(this.androidHome)) { + errors.push({ + warning: "The ANDROID_HOME environment variable is not set or it points to a non-existent directory. You will not be able to perform any build-related operations for Android.", + additionalInformation: "To be able to perform Android build-related operations, set the `ANDROID_HOME` variable to point to the root of your Android SDK installation directory.", + platforms: [Constants.ANDROID_PLATFORM_NAME] + }); + } else if (expectedDirectoriesInAndroidHome.map(dir => this.fs.exists(path.join(this.androidHome, dir))).length === 0) { + errors.push({ + warning: "The ANDROID_HOME environment variable points to incorrect directory. You will not be able to perform any build-related operations for Android.", + additionalInformation: "To be able to perform Android build-related operations, set the `ANDROID_HOME` variable to point to the root of your Android SDK installation directory, " + + "where you will find `tools` and `platform-tools` directories.", + platforms: [Constants.ANDROID_PLATFORM_NAME] + }); + } + + return errors; + } + + public validateMinSupportedTargetSdk({ targetSdk, projectDir }: NativeScriptDoctor.ITargetValidationOptions): NativeScriptDoctor.IWarning[] { + const errors: NativeScriptDoctor.IWarning[] = []; + const newTarget = `${this.ANDROID_TARGET_PREFIX}-${targetSdk}`; + const supportedTargets = this.getSupportedTargets(projectDir); + const targetSupported = _.includes(supportedTargets, newTarget); + + if (!_.includes(supportedTargets, newTarget)) { + const supportedVersions = supportedTargets.sort(); + const minSupportedVersion = this.parseAndroidSdkString(_.first(supportedVersions)); + + if (!targetSupported && targetSdk && (targetSdk < minSupportedVersion)) { + errors.push({ + warning: `The selected Android target SDK ${newTarget} is not supported. You must target ${minSupportedVersion} or later.`, + additionalInformation: "", + platforms: [Constants.ANDROID_PLATFORM_NAME] + }); + } + } + + return errors; + } + + public validataMaxSupportedTargetSdk({ targetSdk, projectDir }: NativeScriptDoctor.ITargetValidationOptions): NativeScriptDoctor.IWarning[] { + const errors: NativeScriptDoctor.IWarning[] = []; + const newTarget = `${this.ANDROID_TARGET_PREFIX}-${targetSdk}`; + const targetSupported = _.includes(this.getSupportedTargets(projectDir), newTarget); + + if (!targetSupported && !targetSdk || targetSdk > this.getMaxSupportedVersion(projectDir)) { + errors.push({ + warning: `Support for the selected Android target SDK ${newTarget} is not verified. Your Android app might not work as expected.`, + additionalInformation: "", + platforms: [Constants.ANDROID_PLATFORM_NAME] + }); + } + + return errors; + } + + public getPathToEmulatorExecutable(): string { + if (!this.pathToEmulatorExecutable) { + const emulatorExecutableName = "emulator"; + + this.pathToEmulatorExecutable = emulatorExecutableName; + + if (this.androidHome) { + // Check https://developer.android.com/studio/releases/sdk-tools.html (25.3.0) + // Since this version of SDK tools, the emulator is a separate package. + // However the emulator executable still exists in the "tools" dir. + const pathToEmulatorFromAndroidStudio = path.join(this.androidHome, emulatorExecutableName, emulatorExecutableName); + const realFilePath = this.hostInfo.isWindows ? `${pathToEmulatorFromAndroidStudio}.exe` : pathToEmulatorFromAndroidStudio; + + if (this.fs.exists(realFilePath)) { + this.pathToEmulatorExecutable = pathToEmulatorFromAndroidStudio; + } else { + this.pathToEmulatorExecutable = path.join(this.androidHome, "tools", emulatorExecutableName); + } + } + } + + return this.pathToEmulatorExecutable; + } + + private getPathToSdkManagementTool(): string { + const sdkmanagerName = "sdkmanager"; + let sdkManagementToolPath = sdkmanagerName; + + const isAndroidHomeValid = this.isAndroidHomeValid(); + + if (isAndroidHomeValid) { + // In case ANDROID_HOME is correct, check if sdkmanager exists and if not it means the SDK has not been updated. + // In this case user shoud use `android` from the command-line instead of sdkmanager. + const pathToSdkmanager = path.join(this.androidHome, "tools", "bin", sdkmanagerName); + const pathToAndroidExecutable = path.join(this.androidHome, "tools", "android"); + const pathToExecutable = this.fs.exists(pathToSdkmanager) ? pathToSdkmanager : pathToAndroidExecutable; + + sdkManagementToolPath = pathToExecutable.replace(this.androidHome, this.hostInfo.isWindows ? "%ANDROID_HOME%" : "$ANDROID_HOME"); + } + + return sdkManagementToolPath; + } + + private getCompileSdk(installedTargets: string[], projectDir: string): number { + const latestValidAndroidTarget = this.getLatestValidAndroidTarget(installedTargets, projectDir); + if (latestValidAndroidTarget) { + const integerVersion = this.parseAndroidSdkString(latestValidAndroidTarget); + + if (integerVersion && integerVersion >= AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET) { + return integerVersion; + } + } + } + + private getMatchingDir(pathToDir: string, versionRange: string): string { + let selectedVersion: string; + if (this.fs.exists(pathToDir)) { + const subDirs = this.fs.readDirectory(pathToDir); + + const subDirsVersions = subDirs + .map(dirName => { + const dirNameGroups = dirName.match(AndroidToolsInfo.VERSION_REGEX); + if (dirNameGroups) { + return dirNameGroups[1]; + } + + return null; + }) + .filter(dirName => !!dirName); + + const version = semver.maxSatisfying(subDirsVersions, versionRange); + if (version) { + selectedVersion = subDirs.find(dir => dir.indexOf(version) !== -1); + } + } + + return selectedVersion; + } + + private getBuildToolsRange(projectDir: string): string { + return `${AndroidToolsInfo.REQUIRED_BUILD_TOOLS_RANGE_PREFIX} <=${this.getMaxSupportedVersion(projectDir)}`; + } + + private getBuildToolsVersion(projectDir: string): string { + let buildToolsVersion: string; + if (this.androidHome) { + const pathToBuildTools = path.join(this.androidHome, "build-tools"); + const buildToolsRange = this.getBuildToolsRange(projectDir); + buildToolsVersion = this.getMatchingDir(pathToBuildTools, buildToolsRange); + } + + return buildToolsVersion; + } + + private getLatestValidAndroidTarget(installedTargets: string[], projectDir: string): string { + return _.findLast(this.getSupportedTargets(projectDir).sort(), supportedTarget => _.includes(installedTargets, supportedTarget)); + } + + private parseAndroidSdkString(androidSdkString: string): number { + return parseInt(androidSdkString.replace(`${this.ANDROID_TARGET_PREFIX}-`, "")); + } + + private getInstalledTargets(): string[] { + try { + const pathToInstalledTargets = path.join(this.androidHome, "platforms"); + if (!this.fs.exists(pathToInstalledTargets)) { + throw new Error("No Android Targets installed."); + } + + return this.fs.readDirectory(pathToInstalledTargets); + } catch (err) { + return []; + } + } + + private getMaxSupportedVersion(projectDir: string): number { + const supportedTargets = this.getSupportedTargets(projectDir); + return this.parseAndroidSdkString(supportedTargets.sort()[supportedTargets.length - 1]); + } + + private getSystemRequirementsLink(): string { + return Constants.SYSTEM_REQUIREMENTS_LINKS[process.platform] || ""; + } + + private isAndroidHomeValid(): boolean { + const errors = this.validateAndroidHomeEnvVariable(); + return !errors && !errors.length; + } + + private getAndroidRuntimePackageFromProjectDir(projectDir: string): { name: string, version: string } { + if (!projectDir || !this.fs.exists(projectDir)) { + return null; + } + const pathToPackageJson = path.join(projectDir, Constants.PACKAGE_JSON); + + if (!this.fs.exists(pathToPackageJson)) { + return null; + } + + const content = this.fs.readJson(pathToPackageJson); + + if (!content) { + return null; + } + + // in case we have a nativescript key and a runtime with a version + // we are dealing with a legacy project and should respect the values + // in the nativescript key + if (content && content.nativescript && content.nativescript['tns-android'] && content.nativescript['tns-android'].version) { + return { + name: Constants.ANDROID_OLD_RUNTIME, + version: content.nativescript && content.nativescript['tns-android'] && content.nativescript['tns-android'].version + }; + } + + if (content && content.devDependencies) { + const foundRuntime = Object.keys(content.devDependencies).find(depName => { + return depName === Constants.ANDROID_SCOPED_RUNTIME || depName === Constants.ANDROID_OLD_RUNTIME; + }); + + if (foundRuntime) { + let version = content.devDependencies[foundRuntime]; + + if (version.includes('tgz')) { + try { + const packagePath = require.resolve(`${foundRuntime}/package.json`, { + paths: [projectDir] + }); + version = require(packagePath).version; + } catch (err) { + version = '*'; + } + } + + return { + name: foundRuntime, + version + }; + } + } + + return null; + } + + private getRuntimeVersion({ runtimeVersion, projectDir }: { runtimeVersion?: string, projectDir?: string }): string { + let runtimePackage = { + name: Constants.ANDROID_SCOPED_RUNTIME, + version: runtimeVersion + }; + if (!runtimeVersion) { + runtimePackage = this.getAndroidRuntimePackageFromProjectDir(projectDir); + runtimeVersion = runtimePackage && runtimePackage.version; + } + + if (runtimeVersion) { + // Check if the version is not "next" or "rc", i.e. tag from npm + if (!semver.validRange(runtimeVersion)) { + try { + const npmViewOutput = this.childProcess.execSync(`npm view ${runtimePackage.name} dist-tags --json`); + const jsonNpmViewOutput = JSON.parse(npmViewOutput); + + runtimeVersion = jsonNpmViewOutput[runtimeVersion] || runtimeVersion; + } catch (err) { + // Maybe there's no npm here + } + } + } + + if (runtimeVersion && !semver.validRange(runtimeVersion)) { + // If we got here, something terribly wrong happened. + throw new Error(`The determined Android runtime version ${runtimeVersion} is not valid. Unable to verify if the current system is setup for Android development.`); + } + + return runtimeVersion; + } + + private getMaxSupportedCompileVersion(config: Partial & { runtimeVersion?: string }): number { + if (config.runtimeVersion && semver.lt(semver.coerce(config.runtimeVersion), "6.1.0")) { + return 28; + } + return this.parseAndroidSdkString(_.last(this.getSupportedTargets(config.projectDir).sort())); + } +} diff --git a/packages/doctor/lib/constants.ts b/packages/doctor/lib/constants.ts new file mode 100644 index 0000000000..226f0dd4aa --- /dev/null +++ b/packages/doctor/lib/constants.ts @@ -0,0 +1,21 @@ +export class Constants { + public static ANDROID_PLATFORM_NAME = "Android"; + public static IOS_PLATFORM_NAME = "iOS"; + public static SUPPORTED_PLATFORMS = [Constants.ANDROID_PLATFORM_NAME, Constants.IOS_PLATFORM_NAME]; + public static SYSTEM_REQUIREMENTS_LINKS: IDictionary = { + "win32": "http://docs.nativescript.org/setup/ns-cli-setup/ns-setup-win.html#system-requirements", + "linux": "http://docs.nativescript.org/setup/ns-cli-setup/ns-setup-linux.html#system-requirements", + "darwin": "http://docs.nativescript.org/setup/ns-cli-setup/ns-setup-os-x.html#system-requirements", + }; + public static INFO_TYPE_NAME = "info"; + public static WARNING_TYPE_NAME = "warning"; + + public static PACKAGE_JSON = "package.json"; + public static NATIVESCRIPT_KEY = "nativescript"; + public static ANDROID_OLD_RUNTIME = "tns-android"; + public static ANDROID_SCOPED_RUNTIME = "@nativescript/android"; + public static VERSION_PROPERTY_NAME = "version"; + public static XCODE_MIN_REQUIRED_VERSION = 10; + public static JAVAC_EXECUTABLE_NAME = "javac"; + public static JAVA_EXECUTABLE_NAME = "java"; +} diff --git a/packages/doctor/lib/declarations.d.ts b/packages/doctor/lib/declarations.d.ts new file mode 100644 index 0000000000..5421682ccb --- /dev/null +++ b/packages/doctor/lib/declarations.d.ts @@ -0,0 +1,83 @@ +/** + * Describes process properties. + */ +interface IProcessInfo { + /** + * The stdout of the process. + */ + stdout: string; + + /** + * The stderr of the process. + */ + stderr: string; + + /** + * The exit code of the process. + */ + exitCode?: number; +} + +interface ISpawnFromEventOptions { + spawnOptions?: any; + ignoreError?: boolean; +} + +/** + * Describes single registry available for search. + */ +interface IHiveId { + /** + * Name of the registry that will be checked. + */ + registry: string; +} + +/** + * Describes available for search registry ids. + */ +interface IHiveIds { + /** + * HKEY_LOCAL_MACHINE + */ + HKLM: IHiveId; + + /** + * HKEY_CURRENT_USER + */ + HKCU: IHiveId; + + /** + * HKEY_CLASSES_ROOT + */ + HKCR: IHiveId; + + /** + * HKEY_CURRENT_CONFIG + */ + HKCC: IHiveId; + + /** + * HKEY_USERS + */ + HKU: IHiveId; +} + +interface IDictionary { + [key: string]: T +} + +interface IVersion { + version: string; +} + +interface INativeScriptNode { + ["tns-android"]: IVersion; + ["tns-ios"]: IVersion; +} + +interface INativeScriptProjectPackageJson { + nativescript: INativeScriptNode; + dependencies?: any; + devDependencies?: { [name: string]: string }; +} diff --git a/packages/doctor/lib/definitions/osenv.d.ts b/packages/doctor/lib/definitions/osenv.d.ts new file mode 100644 index 0000000000..e30314a921 --- /dev/null +++ b/packages/doctor/lib/definitions/osenv.d.ts @@ -0,0 +1,4 @@ +declare module "osenv" { + function home(): string; + function shell(): string; +} diff --git a/packages/doctor/lib/definitions/unzip.d.ts b/packages/doctor/lib/definitions/unzip.d.ts new file mode 100644 index 0000000000..69a7593340 --- /dev/null +++ b/packages/doctor/lib/definitions/unzip.d.ts @@ -0,0 +1,3 @@ +declare module "unzip" { + export function Extract(options: { path: string }): NodeJS.WritableStream; +} diff --git a/packages/doctor/lib/doctor.ts b/packages/doctor/lib/doctor.ts new file mode 100644 index 0000000000..33fd608454 --- /dev/null +++ b/packages/doctor/lib/doctor.ts @@ -0,0 +1,265 @@ +import { Constants } from "./constants"; +import { EOL } from "os"; +import { HostInfo } from "./host-info"; +import { AndroidLocalBuildRequirements } from "./local-build-requirements/android-local-build-requirements"; +import { IosLocalBuildRequirements } from "./local-build-requirements/ios-local-build-requirements"; +import { Helpers } from "./helpers"; +import * as semver from "semver"; + +export class Doctor implements NativeScriptDoctor.IDoctor { + private static MIN_SUPPORTED_POD_VERSION = "1.0.0"; + + constructor(private androidLocalBuildRequirements: AndroidLocalBuildRequirements, + private helpers: Helpers, + private hostInfo: HostInfo, + private iOSLocalBuildRequirements: IosLocalBuildRequirements, + private sysInfo: NativeScriptDoctor.ISysInfo, + private androidToolsInfo: NativeScriptDoctor.IAndroidToolsInfo) { } + + public async canExecuteLocalBuild(platform: string, projectDir?: string, runtimeVersion?: string): Promise { + this.validatePlatform(platform); + + if (platform.toLowerCase() === Constants.ANDROID_PLATFORM_NAME.toLowerCase()) { + return await this.androidLocalBuildRequirements.checkRequirements(projectDir, runtimeVersion); + } else if (platform.toLowerCase() === Constants.IOS_PLATFORM_NAME.toLowerCase()) { + return await this.iOSLocalBuildRequirements.checkRequirements(); + } + + return false; + } + + public async getInfos(config?: NativeScriptDoctor.ISysInfoConfig): Promise { + let result: NativeScriptDoctor.IInfo[] = []; + const sysInfoData = await this.sysInfo.getSysInfo(config); + + if (!config || !config.platform || config.platform.toLowerCase() === Constants.ANDROID_PLATFORM_NAME.toLowerCase()) { + result = result.concat(this.getAndroidInfos(sysInfoData, config && config.projectDir, config && config.androidRuntimeVersion)); + } + + if (!config || !config.platform || config.platform.toLowerCase() === Constants.IOS_PLATFORM_NAME.toLowerCase()) { + result = result.concat(await this.getiOSInfos(sysInfoData)); + } + + if (!this.hostInfo.isDarwin) { + result.push({ + message: "Local builds for iOS can be executed only on a macOS system. To build for iOS on a different operating system, you can use the NativeScript cloud infrastructure.", + additionalInformation: "", + platforms: [Constants.IOS_PLATFORM_NAME], + type: Constants.INFO_TYPE_NAME + }); + } + + return result; + } + + public async getWarnings(config?: NativeScriptDoctor.ISysInfoConfig): Promise { + const info = await this.getInfos(config); + return info.filter(item => item.type === Constants.WARNING_TYPE_NAME) + .map(item => this.convertInfoToWarning(item)); + } + + private getAndroidInfos(sysInfoData: NativeScriptDoctor.ISysInfoData, projectDir?: string, runtimeVersion?: string): NativeScriptDoctor.IInfo[] { + let result: NativeScriptDoctor.IInfo[] = []; + + result = result.concat( + this.processValidationErrors({ + warnings: this.androidToolsInfo.validateAndroidHomeEnvVariable(), + infoMessage: "Your ANDROID_HOME environment variable is set and points to correct directory.", + platforms: [Constants.ANDROID_PLATFORM_NAME] + }), + this.processSysInfoItem({ + item: sysInfoData.adbVer, + infoMessage: "Your adb from the Android SDK is correctly installed.", + warningMessage: "adb from the Android SDK is not installed or is not configured properly. ", + additionalInformation: "For Android-related operations, the NativeScript CLI will use a built-in version of adb." + EOL + + "To avoid possible issues with the native Android emulator, Genymotion or connected" + EOL + + "Android devices, verify that you have installed the latest Android SDK and" + EOL + + "its dependencies as described in http://developer.android.com/sdk/index.html#Requirements", + platforms: [Constants.ANDROID_PLATFORM_NAME] + }), + this.processSysInfoItem({ + item: sysInfoData.isAndroidSdkConfiguredCorrectly, + infoMessage: "The Android SDK is installed.", + warningMessage: "The Android SDK is not installed or is not configured properly.", + additionalInformation: "You will not be able to run your apps in the native emulator. To be able to run apps" + EOL + + "in the native Android emulator, verify that you have installed the latest Android SDK " + EOL + + "and its dependencies as described in http://developer.android.com/sdk/index.html#Requirements", + platforms: [Constants.ANDROID_PLATFORM_NAME] + }), + this.processValidationErrors({ + warnings: this.androidToolsInfo.validateInfo({ projectDir }), + infoMessage: "A compatible Android SDK for compilation is found.", + platforms: [Constants.ANDROID_PLATFORM_NAME] + }), + this.processValidationErrors({ + warnings: this.androidToolsInfo.validateJavacVersion(sysInfoData.javacVersion, projectDir, runtimeVersion), + infoMessage: "Javac is installed and is configured properly.", + platforms: [Constants.ANDROID_PLATFORM_NAME] + }), + this.processSysInfoItem({ + item: sysInfoData.javacVersion, + infoMessage: "The Java Development Kit (JDK) is installed and is configured properly.", + warningMessage: "The Java Development Kit (JDK) is not installed or is not configured properly.", + additionalInformation: "You will not be able to work with the Android SDK and you might not be able" + EOL + + "to perform some Android-related operations. To ensure that you can develop and" + EOL + + "test your apps for Android, verify that you have installed the JDK as" + EOL + + "described in http://docs.oracle.com/javase/8/docs/technotes/guides/install/install_overview.html (for JDK 8).", + platforms: [Constants.ANDROID_PLATFORM_NAME] + }) + ); + + return result; + } + + private async getiOSInfos(sysInfoData: NativeScriptDoctor.ISysInfoData): Promise { + let result: NativeScriptDoctor.IInfo[] = []; + if (this.hostInfo.isDarwin) { + result = result.concat( + this.processSysInfoItem({ + item: sysInfoData.xcodeVer, + infoMessage: "Xcode is installed and is configured properly.", + warningMessage: "Xcode is not installed or is not configured properly.", + additionalInformation: "You will not be able to build your projects for iOS or run them in the iOS Simulator." + EOL + + "To be able to build for iOS and run apps in the native emulator, verify that you have installed Xcode.", + platforms: [Constants.IOS_PLATFORM_NAME] + }), + this.processSysInfoItem({ + item: sysInfoData.xcodeprojLocation, + infoMessage: "xcodeproj is installed and is configured properly.", + warningMessage: "xcodeproj is not installed or is not configured properly.", + additionalInformation: "You will not be able to build your projects for iOS." + EOL + + "To be able to build for iOS and run apps in the native emulator, verify that you have installed xcodeproj.", + platforms: [Constants.IOS_PLATFORM_NAME] + }), + this.processSysInfoItem({ + item: sysInfoData.cocoaPodsVer, + infoMessage: "CocoaPods are installed.", + warningMessage: "CocoaPods is not installed or is not configured properly.", + additionalInformation: "You will not be able to build your projects for iOS if they contain plugin with CocoaPod file." + EOL + + "To be able to build such projects, verify that you have installed CocoaPods (`sudo gem install cocoapods`).", + platforms: [Constants.IOS_PLATFORM_NAME] + }), + this.processSysInfoItem({ + item: !sysInfoData.cocoaPodsVer || !sysInfoData.isCocoaPodsUpdateRequired, + infoMessage: "CocoaPods update is not required.", + warningMessage: "CocoaPods update required.", + additionalInformation: `You are using CocoaPods version ${sysInfoData.cocoaPodsVer} which does not support Xcode ${sysInfoData.xcodeVer} yet.${EOL}${EOL}You can update your cocoapods by running $sudo gem install cocoapods from a terminal.${EOL}${EOL}In order for the NativeScript CLI to be able to work correctly with this setup you need to install xcproj command line tool and add it to your PATH.Xcproj can be installed with homebrew by running $ brew install xcproj from the terminal`, + platforms: [Constants.IOS_PLATFORM_NAME] + }) + ); + + if (sysInfoData.xcodeVer && sysInfoData.cocoaPodsVer) { + const isCocoaPodsWorkingCorrectly = await this.sysInfo.isCocoaPodsWorkingCorrectly(); + result = result.concat( + this.processSysInfoItem({ + item: isCocoaPodsWorkingCorrectly, + infoMessage: "CocoaPods are configured properly.", + warningMessage: "There was a problem with CocoaPods", + additionalInformation: "Verify that CocoaPods are configured properly.", + platforms: [Constants.IOS_PLATFORM_NAME], + }) + ); + } + + result = result.concat( + this.processSysInfoItem({ + item: !sysInfoData.cocoaPodsVer || !semver.valid(sysInfoData.cocoaPodsVer) || !semver.lt(sysInfoData.cocoaPodsVer, Doctor.MIN_SUPPORTED_POD_VERSION), + infoMessage: `Your current CocoaPods version is newer than ${Doctor.MIN_SUPPORTED_POD_VERSION}.`, + warningMessage: `Your current CocoaPods version is earlier than ${Doctor.MIN_SUPPORTED_POD_VERSION}.`, + additionalInformation: "You will not be able to build your projects for iOS if they contain plugin with CocoaPod file." + EOL + + `To be able to build such projects, verify that you have at least ${Doctor.MIN_SUPPORTED_POD_VERSION} version installed.`, + platforms: [Constants.IOS_PLATFORM_NAME] + }), + this.processSysInfoItem({ + item: sysInfoData.pythonInfo.isInstalled, + infoMessage: "Python installed and configured correctly.", + warningMessage: `Couldn't retrieve installed python packages.`, + additionalInformation: "We cannot verify your python installation is setup correctly. Please, make sure you have both 'python' and 'pip' installed." + EOL + + `Error while validating Python packages. Error is: ${sysInfoData.pythonInfo.installationErrorMessage}`, + platforms: [Constants.IOS_PLATFORM_NAME] + }), + this.processSysInfoItem({ + item: sysInfoData.pythonInfo.isSixPackageInstalled, + infoMessage: `The Python 'six' package is found.`, + warningMessage: `The Python 'six' package not found.`, + additionalInformation: "This package is required by the Debugger library (LLDB) for iOS. You can install it by first making sure you have pip installed and then running 'pip install six' from the terminal.", + platforms: [Constants.IOS_PLATFORM_NAME] + }) + ); + + if (sysInfoData.xcodeVer) { + result = result.concat( + this.processSysInfoItem({ + item: await this.iOSLocalBuildRequirements.isXcodeVersionValid(), + infoMessage: `Xcode version ${sysInfoData.xcodeVer} satisfies minimum required version ${Constants.XCODE_MIN_REQUIRED_VERSION}.`, + warningMessage: `Xcode version ${sysInfoData.xcodeVer} is lower than minimum required version ${Constants.XCODE_MIN_REQUIRED_VERSION}.`, + additionalInformation: "To build your application for iOS, update your Xcode.", + platforms: [Constants.IOS_PLATFORM_NAME] + }) + ); + } + } + + return result; + } + + private processSysInfoItem(data: { item: string | boolean, infoMessage: string, warningMessage: string, additionalInformation?: string, platforms: string[] }): NativeScriptDoctor.IInfo { + if (!data.item) { + return { + message: `WARNING: ${data.warningMessage}`, + additionalInformation: data.additionalInformation, + platforms: data.platforms, + type: Constants.WARNING_TYPE_NAME + }; + } + + return { + message: `${data.infoMessage}`, + platforms: data.platforms, + type: Constants.INFO_TYPE_NAME + }; + } + + private processValidationErrors(data: { warnings: NativeScriptDoctor.IWarning[], infoMessage: string, platforms: string[] }): NativeScriptDoctor.IInfo[] { + if (data.warnings.length > 0) { + return data.warnings.map(warning => this.convertWarningToInfo(warning)); + } + + return [{ + message: data.infoMessage, + platforms: data.platforms, + type: Constants.INFO_TYPE_NAME + }]; + } + + private convertWarningToInfo(warning: NativeScriptDoctor.IWarning): NativeScriptDoctor.IInfo { + return { + message: warning.warning, + additionalInformation: warning.additionalInformation, + platforms: warning.platforms, + type: Constants.WARNING_TYPE_NAME + }; + } + + private convertInfoToWarning(info: NativeScriptDoctor.IInfo): NativeScriptDoctor.IWarning { + return { + warning: info.message, + additionalInformation: info.additionalInformation, + platforms: info.platforms + }; + } + + private isPlatformSupported(platform: string): boolean { + return Constants.SUPPORTED_PLATFORMS.map(pl => pl.toLowerCase()).indexOf(platform.toLowerCase()) !== -1; + } + + private validatePlatform(platform: string): void { + if (!platform) { + throw new Error("You must specify a platform."); + } + + if (!this.isPlatformSupported(platform)) { + throw new Error(`Platform ${platform} is not supported.The supported platforms are: ${Constants.SUPPORTED_PLATFORMS.join(", ")} `); + } + } +} diff --git a/packages/doctor/lib/helpers.ts b/packages/doctor/lib/helpers.ts new file mode 100644 index 0000000000..d72bf9de86 --- /dev/null +++ b/packages/doctor/lib/helpers.ts @@ -0,0 +1,57 @@ +import { HostInfo } from "./host-info"; + +export class Helpers { + constructor(private hostInfo: HostInfo) { } + + public getPropertyName(method: Function): string { + if (method) { + const match = method.toString().match(/(?:return\s+?.*\.(.+);)|(?:=>\s*?.*\.(.+)\b)/); + if (match) { + return (match[1] || match[2]).trim(); + } + } + + return null; + } + + public quoteString(value: string): string { + if (!value) { + return value; + } + + return this.hostInfo.isWindows ? this.cmdQuote(value) : this.bashQuote(value); + } + + /** + * Appends zeroes to a version string until it reaches a specified length. + * @param {string} version The version on which to append zeroes. + * @param {number} requiredVersionLength The required length of the version string. + * @returns {string} Appended version string. In case input is null, undefined or empty string, it is returned immediately without appending anything. + */ + public appendZeroesToVersion(version: string, requiredVersionLength: number): string { + if (version) { + const zeroesToAppend = requiredVersionLength - version.split(".").length; + for (let index = 0; index < zeroesToAppend; index++) { + version += ".0"; + } + } + + return version; + } + + private bashQuote(s: string): string { + if (s[0] === "'" && s[s.length - 1] === "'") { + return s; + } + // replace ' with '"'"' and wrap in '' + return "'" + s.replace(/'/g, '\'"\'"\'') + "'"; + } + + private cmdQuote(s: string): string { + if (s[0] === '"' && s[s.length - 1] === '"') { + return s; + } + // replace " with \" and wrap in "" + return '"' + s.replace(/"/g, '\\"') + '"'; + } +} diff --git a/packages/doctor/lib/host-info.ts b/packages/doctor/lib/host-info.ts new file mode 100644 index 0000000000..ff08a7022f --- /dev/null +++ b/packages/doctor/lib/host-info.ts @@ -0,0 +1,43 @@ +import { WinReg } from "./winreg"; + +export class HostInfo { + private static WIN32_NAME = "win32"; + private static PROCESSOR_ARCHITEW6432 = "PROCESSOR_ARCHITEW6432"; + private static DARWIN_OS_NAME = "darwin"; + private static LINUX_OS_NAME = "linux"; + private static DOT_NET_REGISTRY_PATH = "\\Software\\Microsoft\\NET Framework Setup\\NDP\\v4\\Client"; + + constructor(private winreg: WinReg) { } + + public get isWindows(): boolean { + return process.platform === HostInfo.WIN32_NAME; + } + + public get isWindows64(): boolean { + return this.isWindows && (this.isNode64Bit || process.env.hasOwnProperty(HostInfo.PROCESSOR_ARCHITEW6432)); + } + + public get isWindows32() { + return this.isWindows && !this.isWindows64; + } + + public get isDarwin(): boolean { + return process.platform === HostInfo.DARWIN_OS_NAME; + } + + public get isLinux(): boolean { + return process.platform === HostInfo.LINUX_OS_NAME; + } + + public get isNode64Bit(): boolean { + return process.arch === "x64"; + } + + public dotNetVersion(): Promise { + if (this.isWindows) { + return this.winreg.getRegistryValue("Version", this.winreg.registryKeys.HKLM, HostInfo.DOT_NET_REGISTRY_PATH); + } else { + return Promise.resolve(null); + } + } +} diff --git a/packages/doctor/lib/index.ts b/packages/doctor/lib/index.ts new file mode 100644 index 0000000000..e785e0dbd7 --- /dev/null +++ b/packages/doctor/lib/index.ts @@ -0,0 +1,35 @@ +import { ChildProcess } from "./wrappers/child-process"; +import { FileSystem } from "./wrappers/file-system"; +import { SysInfo } from "./sys-info"; +import { HostInfo } from "./host-info"; +import { AndroidToolsInfo } from "./android-tools-info"; +import { WinReg } from "./winreg"; +import { Helpers } from "./helpers"; +import { Doctor } from "./doctor"; +import { AndroidLocalBuildRequirements } from "./local-build-requirements/android-local-build-requirements"; +import { IosLocalBuildRequirements } from "./local-build-requirements/ios-local-build-requirements"; +import { Constants as constants } from "./constants"; + +const childProcess = new ChildProcess(); +const winReg = new WinReg(); +const hostInfo = new HostInfo(winReg); +const fileSystem = new FileSystem(); +const helpers = new Helpers(hostInfo); +const androidToolsInfo = new AndroidToolsInfo(childProcess, fileSystem, hostInfo, helpers); + +const sysInfo: NativeScriptDoctor.ISysInfo = new SysInfo(childProcess, fileSystem, helpers, hostInfo, winReg, androidToolsInfo); + +const androidLocalBuildRequirements = new AndroidLocalBuildRequirements(androidToolsInfo, sysInfo); +const iOSLocalBuildRequirements = new IosLocalBuildRequirements(sysInfo, hostInfo); + +const doctor: NativeScriptDoctor.IDoctor = new Doctor(androidLocalBuildRequirements, helpers, hostInfo, iOSLocalBuildRequirements, sysInfo, androidToolsInfo); + +const setShouldCacheSysInfo = sysInfo.setShouldCacheSysInfo.bind(sysInfo); + +export { + sysInfo, + doctor, + constants, + setShouldCacheSysInfo, + androidToolsInfo +}; diff --git a/packages/doctor/lib/local-build-requirements/android-local-build-requirements.ts b/packages/doctor/lib/local-build-requirements/android-local-build-requirements.ts new file mode 100644 index 0000000000..fc4ccbff92 --- /dev/null +++ b/packages/doctor/lib/local-build-requirements/android-local-build-requirements.ts @@ -0,0 +1,17 @@ +export class AndroidLocalBuildRequirements { + constructor(private androidToolsInfo: NativeScriptDoctor.IAndroidToolsInfo, + private sysInfo: NativeScriptDoctor.ISysInfo) { } + + public async checkRequirements(projectDir?: string, runtimeVersion?: string): Promise { + const androidToolsInfo = await this.androidToolsInfo.validateInfo({projectDir}); + const javacVersion = await this.sysInfo.getJavaCompilerVersion(); + const isJavacVersionInvalid = !javacVersion || (await this.androidToolsInfo.validateJavacVersion(javacVersion, projectDir, runtimeVersion)).length; + if (androidToolsInfo.length || + !await this.sysInfo.getAdbVersion() || + isJavacVersionInvalid) { + return false; + } + + return true; + } +} diff --git a/packages/doctor/lib/local-build-requirements/ios-local-build-requirements.ts b/packages/doctor/lib/local-build-requirements/ios-local-build-requirements.ts new file mode 100644 index 0000000000..cff27d5b40 --- /dev/null +++ b/packages/doctor/lib/local-build-requirements/ios-local-build-requirements.ts @@ -0,0 +1,24 @@ +import { HostInfo } from "../host-info"; +import * as semver from "semver"; +import { Constants } from "../constants"; + +export class IosLocalBuildRequirements { + constructor(private sysInfo: NativeScriptDoctor.ISysInfo, + private hostInfo: HostInfo) { } + + public async checkRequirements(): Promise { + if (!this.hostInfo.isDarwin || + !await this.isXcodeVersionValid() || + !await this.sysInfo.getXcodeprojLocation()) { + return false; + } + + return true; + } + + public async isXcodeVersionValid(): Promise { + const xcodeVersion = await this.sysInfo.getXcodeVersion(); + + return !!xcodeVersion && (semver.major(semver.coerce(xcodeVersion)) >= Constants.XCODE_MIN_REQUIRED_VERSION); + } +} diff --git a/packages/doctor/lib/sys-info.ts b/packages/doctor/lib/sys-info.ts new file mode 100644 index 0000000000..69d33bf706 --- /dev/null +++ b/packages/doctor/lib/sys-info.ts @@ -0,0 +1,595 @@ +import { ChildProcess } from "./wrappers/child-process"; +import { FileSystem } from "./wrappers/file-system"; +import { HostInfo } from "./host-info"; +import { ExecOptions } from "child_process"; +import { WinReg } from "./winreg"; +import { Helpers } from "./helpers"; +import { platform, EOL } from "os"; +import * as path from "path"; +import * as osenv from "osenv"; +import * as temp from "temp"; +import * as semver from "semver"; +import { Constants } from "./constants"; + +export class SysInfo implements NativeScriptDoctor.ISysInfo { + private static JAVA_COMPILER_VERSION_REGEXP = /^javac (.*)/im; + private static JAVA_VERSION_REGEXP = /^(?:(?:java)|(?:openjdk)).*?\"(.*)\"/im; + private static XCODE_VERSION_REGEXP = /Xcode (.*)/; + private static VERSION_REGEXP = /(\d{1,})\.(\d{1,})\.*([\w-]{0,})/m; + private static CLI_OUTPUT_VERSION_REGEXP = /^(?:\d+\.){2}\d+.*?$/m; + private static GIT_VERSION_REGEXP = /^git version (.*)/; + private static GRADLE_VERSION_REGEXP = /Gradle (.*)/i; + + private monoVerRegExp = /version (\d+[.]\d+[.]\d+) /gm; + + private javaCompilerVerCache: string; + private javaPathCache: string; + private javaVerCache: string; + private javaVerJavaHomeCache: string; + private javaVerPathCache: string; + private xCodeVerCache: string; + private npmVerCache: string; + private nodeVerCache: string; + private nodeGypVerCache: string; + private xCodeprojLocationCache: string; + private iTunesInstalledCache: boolean; + private cocoaPodsVerCache: string; + private osCache: string; + private adbVerCache: string; + private androidInstalledCache: boolean; + private monoVerCache: string; + private gitVerCache: string; + private gradleVerCache: string; + + private commonSysInfoCache: NativeScriptDoctor.ICommonSysInfoData; + private androidSysInfoCache: NativeScriptDoctor.IAndroidSysInfoData; + private iOSSysInfoCache: NativeScriptDoctor.IiOSSysInfoData; + + private isCocoaPodsWorkingCorrectlyCache: boolean; + private nativeScriptCliVersionCache: string; + private xcprojInfoCache: NativeScriptDoctor.IXcprojInfo; + private pythonInfoCache: NativeScriptDoctor.IPythonInfo; + private isCocoaPodsUpdateRequiredCache: boolean; + private shouldCache: boolean = true; + private isAndroidSdkConfiguredCorrectlyCache: boolean; + + constructor(private childProcess: ChildProcess, + private fileSystem: FileSystem, + private helpers: Helpers, + private hostInfo: HostInfo, + private winReg: WinReg, + private androidToolsInfo: NativeScriptDoctor.IAndroidToolsInfo) { } + + public getJavaCompilerVersion(): Promise { + return this.getValueForProperty(() => this.javaCompilerVerCache, async (): Promise => { + const javacVersion = process.env["JAVA_HOME"] ? (await this.getVersionOfJavaExecutableFromJavaHome(Constants.JAVAC_EXECUTABLE_NAME, SysInfo.JAVA_COMPILER_VERSION_REGEXP)) : + (await this.getVersionOfJavaExecutableFromPath(Constants.JAVAC_EXECUTABLE_NAME, SysInfo.JAVA_COMPILER_VERSION_REGEXP)); + + return javacVersion; + }); + } + + public getJavaVersion(): Promise { + return this.getValueForProperty(() => this.javaVerCache, async (): Promise => { + const javaVersion = (await this.getJavaVersionFromJavaHome()) || (await this.getJavaVersionFromPath()); + return javaVersion; + }); + } + + public getJavaPath(): Promise { + return this.getValueForProperty(() => this.javaPathCache, async (): Promise => { + const javaPath = (await this.getJavaExecutablePathFromJavaHome(Constants.JAVA_EXECUTABLE_NAME)) || (await this.getJavaExecutablePathFromPath(Constants.JAVA_EXECUTABLE_NAME)); + return javaPath; + }); + } + + public getJavaVersionFromPath(): Promise { + return this.getValueForProperty(() => this.javaVerPathCache, (): Promise => { + return this.getVersionOfJavaExecutableFromPath(Constants.JAVA_EXECUTABLE_NAME, SysInfo.JAVA_VERSION_REGEXP); + }); + } + + public getJavaVersionFromJavaHome(): Promise { + return this.getValueForProperty(() => this.javaVerJavaHomeCache, (): Promise => { + return this.getVersionOfJavaExecutableFromJavaHome(Constants.JAVA_EXECUTABLE_NAME, SysInfo.JAVA_VERSION_REGEXP); + }); + } + + public getXcodeVersion(): Promise { + return this.getValueForProperty(() => this.xCodeVerCache, async (): Promise => { + if (this.hostInfo.isDarwin) { + const output = await this.execCommand("xcodebuild -version"); + const xcodeVersionMatch = output && output.match(SysInfo.XCODE_VERSION_REGEXP); + + if (xcodeVersionMatch) { + return this.getVersionFromString(output); + } + } + }); + } + + public async getNodeVersion(): Promise { + return this.getValueForProperty(() => this.nodeVerCache, async (): Promise => { + const output = await this.execCommand("node -v"); + if (output) { + const version = this.getVersionFromString(output); + return version || output; + } + + return null; + }); + } + + public getNpmVersion(): Promise { + return this.getValueForProperty(() => this.npmVerCache, async (): Promise => { + const output = await this.execCommand("npm -v"); + return output ? output.split("\n")[0] : null; + }); + } + + public getNodeGypVersion(): Promise { + return this.getValueForProperty(() => this.nodeGypVerCache, async (): Promise => { + const output = await this.execCommand("node-gyp -v"); + return output ? this.getVersionFromString(output) : null; + }); + } + + public getXcodeprojLocation(): Promise { + return this.getValueForProperty(() => this.xCodeprojLocationCache, async (): Promise => { + const output = await this.execCommand("which xcodeproj"); + return output ? output.trim() : null; + }); + } + + public isITunesInstalled(): Promise { + return this.getValueForProperty(() => this.iTunesInstalledCache, async (): Promise => { + if (this.hostInfo.isLinux) { + return false; + } + + let coreFoundationDir: string; + let mobileDeviceDir: string; + + if (this.hostInfo.isWindows) { + const commonProgramFiles = this.hostInfo.isWindows64 ? process.env["CommonProgramFiles(x86)"] : process.env["CommonProgramFiles"]; + coreFoundationDir = path.join(commonProgramFiles, "Apple", "Apple Application Support"); + mobileDeviceDir = path.join(commonProgramFiles, "Apple", "Mobile Device Support"); + } else if (this.hostInfo.isDarwin) { + coreFoundationDir = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + mobileDeviceDir = "/System/Library/PrivateFrameworks/MobileDevice.framework/MobileDevice"; + } + + return await this.fileSystem.exists(coreFoundationDir) && await this.fileSystem.exists(mobileDeviceDir); + }); + } + + public getCocoaPodsVersion(): Promise { + return this.getValueForProperty(() => this.cocoaPodsVerCache, async (): Promise => { + if (this.hostInfo.isDarwin) { + if (this.hostInfo.isDarwin) { + const output = await this.execCommand("pod --version"); + // Output of pod --version could contain some warnings. Find the version in it. + const cocoaPodsVersionMatch = output && output.match(SysInfo.VERSION_REGEXP); + if (cocoaPodsVersionMatch && cocoaPodsVersionMatch[0]) { + return cocoaPodsVersionMatch[0].trim(); + } + } + } + }); + } + + public getOs(): Promise { + return this.getValueForProperty(() => this.osCache, async (): Promise => { + return await (this.hostInfo.isWindows ? this.winVer() : this.unixVer()); + }); + } + + public getAdbVersion(pathToAdb?: string): Promise { + return this.getValueForProperty(() => this.adbVerCache, async (): Promise => { + let output: IProcessInfo = null; + + pathToAdb = pathToAdb || await this.androidToolsInfo.getPathToAdbFromAndroidHome(); + if (pathToAdb) { + output = await this.childProcess.spawnFromEvent(pathToAdb, ["version"], "close", { ignoreError: true }); + } + + return output && output.stdout ? this.getVersionFromString(output.stdout) : null; + }); + } + + public isAndroidInstalled(): Promise { + return this.getValueForProperty(() => this.androidInstalledCache, async (): Promise => { + try { + const errors = this.androidToolsInfo.validateAndroidHomeEnvVariable(); + return errors.length === 0; + } catch (err) { + return false; + } + }); + } + + public async isAndroidSdkConfiguredCorrectly(): Promise { + return this.getValueForProperty(() => this.isAndroidSdkConfiguredCorrectlyCache, async (): Promise => { + const output = await this.childProcess.spawnFromEvent(this.androidToolsInfo.getPathToEmulatorExecutable(), ["-help"], "close", { ignoreError: true }); + + return output && output.stdout.indexOf("usage: emulator") >= 0; + }); + } + + public getMonoVersion(): Promise { + return this.getValueForProperty(() => this.monoVerCache, async (): Promise => { + const output = await this.execCommand("mono --version"); + const match = this.monoVerRegExp.exec(output); + return match ? match[1] : null; + }); + } + + public getGitVersion(): Promise { + return this.getValueForProperty(() => this.gitVerCache, async (): Promise => { + const gitPath = await this.getGitPath(); + if (!gitPath) { + return null; + } + + const output = await this.execCommand(`${this.helpers.quoteString(gitPath)} --version`); + const matches = SysInfo.GIT_VERSION_REGEXP.exec(output); + return matches && matches[1]; + }); + } + + public getGradleVersion(): Promise { + return this.getValueForProperty(() => this.gradleVerCache, async (): Promise => { + const output = await this.execCommand("gradle -v"); + const matches = SysInfo.GRADLE_VERSION_REGEXP.exec(output); + + return matches && matches[1]; + }); + } + + public async getSysInfo(config?: NativeScriptDoctor.ISysInfoConfig): Promise { + if (config && config.platform && config.platform.toLowerCase() === Constants.ANDROID_PLATFORM_NAME.toLowerCase()) { + return Object.assign(await this.getCommonSysInfo(), await this.getAndroidSysInfo(config)); + } + + if (config && config.platform && config.platform.toLowerCase() === Constants.IOS_PLATFORM_NAME.toLowerCase()) { + return Object.assign(await this.getCommonSysInfo(), await this.getiOSSysInfo()); + } + + return Object.assign(await this.getCommonSysInfo(), await this.getAndroidSysInfo(), await this.getiOSSysInfo()); + } + + public isCocoaPodsWorkingCorrectly(): Promise { + return this.getValueForProperty(() => this.isCocoaPodsWorkingCorrectlyCache, async (): Promise => { + if (this.hostInfo.isDarwin) { + if (!this.fileSystem.exists(path.join(osenv.home(), ".cocoapods"))) { + return true; + } + temp.track(); + const tempDirectory = temp.mkdirSync("nativescript-check-cocoapods"); + const pathToXCodeProjectZip = path.join(__dirname, "..", "resources", "cocoapods-verification", "cocoapods.zip"); + + await this.fileSystem.extractZip(pathToXCodeProjectZip, tempDirectory); + + const xcodeProjectDir = path.join(tempDirectory, "cocoapods"); + + try { + const spawnResult = await this.childProcess.spawnFromEvent("pod", ["install"], "exit", { spawnOptions: { cwd: xcodeProjectDir } }); + if (spawnResult.exitCode) { + this.fileSystem.deleteEntry(tempDirectory); + return false; + } else { + const exists = this.fileSystem.exists(path.join(xcodeProjectDir, "cocoapods.xcworkspace")); + this.fileSystem.deleteEntry(tempDirectory); + return exists; + } + } catch (err) { + this.fileSystem.deleteEntry(tempDirectory); + return null; + } + } else { + return false; + } + }); + } + + public getNativeScriptCliVersion(): Promise { + return this.getValueForProperty(() => this.nativeScriptCliVersionCache, async (): Promise => { + const output = await this.execCommand("tns --version"); + return output ? this.getVersionFromCLIOutput(output.trim()) : output; + }); + } + + public getXcprojInfo(): Promise { + return this.getValueForProperty(() => this.xcprojInfoCache, async (): Promise => { + const cocoaPodsVersion = await this.getCocoaPodsVersion(); + let xcodeVersion = await this.getXcodeVersion(); + if (xcodeVersion) { + xcodeVersion = this.helpers.appendZeroesToVersion(xcodeVersion, 3); + } + + // CocoaPods with version lower than 1.0.0 don't support Xcode 7.3 yet + // https://github.com/CocoaPods/CocoaPods/issues/2530#issuecomment-210470123 + // as a result of this all .pbxprojects touched by CocoaPods get converted to XML plist format + const shouldUseXcproj = cocoaPodsVersion && !!(semver.lt(cocoaPodsVersion, "1.0.0") && semver.gte(xcodeVersion, "7.3.0")); + let xcprojAvailable: boolean; + + if (shouldUseXcproj) { + // If that's the case we can use xcproj gem to convert them back to ASCII plist format + xcprojAvailable = !!(await this.exec("xcproj --version")); + } + + return { shouldUseXcproj, xcprojAvailable }; + }); + } + + public getPythonInfo(): Promise { + return this.getValueForProperty(() => this.pythonInfoCache, async (): Promise => { + if (this.hostInfo.isDarwin) { + try { + await this.childProcess.exec(`python -c "import six"`); + } catch (error) { + // error.code = 1 so the Python is present, but we don't have six. + if (error.code === 1) { + return { isInstalled: true, isSixPackageInstalled: false }; + } + + return { isInstalled: false, isSixPackageInstalled: false, installationErrorMessage: error.message }; + } + + return { isInstalled: true, isSixPackageInstalled: true }; + } + + return null; + }); + } + + public isCocoaPodsUpdateRequired(): Promise { + return this.getValueForProperty(() => this.isCocoaPodsUpdateRequiredCache, async (): Promise => { + const xcprojInfo = await this.getXcprojInfo(); + if (xcprojInfo.shouldUseXcproj && !xcprojInfo.xcprojAvailable) { + return true; + } else { + return false; + } + }); + } + + public setShouldCacheSysInfo(shouldCache: boolean): void { + this.shouldCache = shouldCache; + } + + public getGitPath(): Promise { + return this.hostInfo.isWindows ? this.findGitWin32() : this.findGitUnix(); + } + + private async findGitWin32(): Promise { + let result: string; + const win32Paths = [process.env["ProgramFiles"], process.env["ProgramFiles(x86)"]]; + for (const win32Path of win32Paths) { + result = this.findSystemGitWin32(win32Path); + if (result) { + return result; + } + } + + result = this.findGitHubGitWin32(); + return result ? result : await this.findGitCore("where"); + } + + private findSystemGitWin32(base: string): string { + if (!base) { + return null; + } + + return this.findSpecificGit(path.join(base, "Git", "cmd", "git.exe")); + } + + private findGitHubGitWin32(): string { + const github = path.join(process.env["LOCALAPPDATA"], "GitHub"); + if (!this.fileSystem.exists(github)) { + return null; + } + + const children = this.fileSystem.readDirectory(github); + const git = children.filter(child => /^PortableGit/.test(child))[0]; + if (!this.fileSystem.exists(git)) { + return null; + } + + return this.findSpecificGit(path.join(github, git, "cmd", "git.exe")); + } + + private findSpecificGit(gitPath: string): string { + return this.fileSystem.exists(gitPath) ? gitPath : null; + } + + private async findGitUnix(): Promise { + return await this.findGitCore("which"); + } + + private async findGitCore(command: string, options?: any): Promise { + const result = await this.execCommand(`${command} git`); + + return result && result.split("\n")[0].trim(); + } + + private async getValueForProperty(property: Function, getValueMethod: () => Promise): Promise { + if (this.shouldCache) { + const propertyName = this.helpers.getPropertyName(property); + const cachedValue: T = (this)[propertyName]; + + if (cachedValue === undefined) { + const result = await getValueMethod(); + (this)[propertyName] = result; + return result; + } else { + return cachedValue; + } + } else { + return await getValueMethod(); + } + } + + private async exec(cmd: string, execOptions?: ExecOptions): Promise { + if (cmd) { + try { + return await this.childProcess.exec(cmd, execOptions); + } catch (err) { + return null; + } + } + + return null; + } + + private async execCommand(cmd: string, execOptions?: ExecOptions): Promise { + const output = await this.exec(cmd, execOptions); + return output && output.stdout; + } + + private getVersionFromString(versionString: string): string { + const matches = versionString.match(SysInfo.VERSION_REGEXP); + if (matches) { + return `${matches[1]}.${matches[2]}.${matches[3] || 0}`; + } + + return null; + } + + private getVersionFromCLIOutput(commandOutput: string): string { + const matches = commandOutput.match(SysInfo.CLI_OUTPUT_VERSION_REGEXP); + return matches && matches[0]; + } + + private async winVer(): Promise { + let productName: string; + let currentVersion: string; + let currentBuild: string; + const hive = this.winReg.registryKeys.HKLM; + const key = "\\Software\\Microsoft\\Windows NT\\CurrentVersion"; + + productName = await this.winReg.getRegistryValue("ProductName", hive, key); + currentVersion = await this.winReg.getRegistryValue("CurrentVersion", hive, key); + currentBuild = await this.winReg.getRegistryValue("CurrentBuild", hive, key); + + return `${productName} ${currentVersion}.${currentBuild}`; + } + + private unixVer(): Promise { + return this.execCommand("uname -a"); + } + + private getCommonSysInfo(): Promise { + return this.getValueForProperty(() => this.commonSysInfoCache, async (): Promise => { + const result: NativeScriptDoctor.ICommonSysInfoData = Object.create(null); + + // os stuff + result.platform = platform(); + result.shell = osenv.shell(); + result.os = await this.getOs(); + result.procArch = process.arch; + result.nodeVer = await this.getNodeVersion(); + result.npmVer = await this.getNpmVersion(); + result.nodeGypVer = await this.getNodeGypVersion(); + result.nativeScriptCliVersion = await this.getNativeScriptCliVersion(); + result.gitVer = await this.getGitVersion(); + + return result; + }); + } + + private async getiOSSysInfo(): Promise { + return this.getValueForProperty(() => this.iOSSysInfoCache, async (): Promise => { + const result: NativeScriptDoctor.IiOSSysInfoData = Object.create(null); + + result.xcodeVer = await this.getXcodeVersion(); + result.xcodeprojLocation = await this.getXcodeprojLocation(); + result.itunesInstalled = await this.isITunesInstalled(); + result.cocoaPodsVer = await this.getCocoaPodsVersion(); + result.isCocoaPodsWorkingCorrectly = await this.isCocoaPodsWorkingCorrectly(); + result.isCocoaPodsUpdateRequired = await this.isCocoaPodsUpdateRequired(); + result.pythonInfo = await this.getPythonInfo(); + + return result; + }); + } + + private async getAndroidSysInfo(config?: NativeScriptDoctor.ISysInfoConfig): Promise { + return this.getValueForProperty(() => this.androidSysInfoCache, async (): Promise => { + const result: NativeScriptDoctor.IAndroidSysInfoData = Object.create(null); + + result.dotNetVer = await this.hostInfo.dotNetVersion(); + result.javacVersion = await this.getJavaCompilerVersion(); + result.javaVersion = await this.getJavaVersion(); + result.javaPath = await this.getJavaPath(); + result.adbVer = await this.getAdbVersion(config && config.androidToolsInfo && config.androidToolsInfo.pathToAdb); + result.androidInstalled = await this.isAndroidInstalled(); + result.monoVer = await this.getMonoVersion(); + result.gradleVer = await this.getGradleVersion(); + result.isAndroidSdkConfiguredCorrectly = await this.isAndroidSdkConfiguredCorrectly(); + + return result; + }); + } + + private async getVersionOfJavaExecutableFromJavaHome(javaExecutableName: string, regExp: RegExp): Promise { + let javaExecutableVersion: string = null; + const javaExecutablePath = this.getJavaExecutablePathFromJavaHome(javaExecutableName); + if (javaExecutablePath) { + javaExecutableVersion = await this.getVersionOfJavaExecutable(javaExecutablePath, regExp); + } + + return javaExecutableVersion; + } + + private getJavaExecutablePathFromJavaHome(javaExecutableName: string): string { + let javaExecutablePath: string = null; + + try { + const javaHome = process.env["JAVA_HOME"]; + const javaExecutableFile = this.hostInfo.isWindows ? `${javaExecutableName}.exe` : javaExecutableName; + + if (javaHome) { + const pathToJavaExecutable = path.join(javaHome, "bin", javaExecutableFile); + if (this.fileSystem.exists(pathToJavaExecutable)) { + javaExecutablePath = pathToJavaExecutable; + } + } + } catch (err) { /* intentionally left blank */ } + + return javaExecutablePath; + } + + private async getVersionOfJavaExecutableFromPath(javaExecutableName: string, regExp: RegExp): Promise { + let javaExecutableVersion: string = null; + + const javaExecutablePath = await this.getJavaExecutablePathFromPath(javaExecutableName); + if (javaExecutablePath) { + javaExecutableVersion = await this.getVersionOfJavaExecutable(javaExecutablePath, regExp); + } + + return javaExecutableVersion; + } + + private async getJavaExecutablePathFromPath(javaExecutableName: string): Promise { + let javaExecutablePath: string = null; + + try { + const detectionCommand = this.hostInfo.isWindows ? "where" : "which"; + // if this command succeeds, we have javac in the PATH. In case it is not there, it will throw an error. + await this.childProcess.exec(`${detectionCommand} ${javaExecutableName}`); + javaExecutablePath = javaExecutableName; + } catch (err) { /* intentionally left blank */ } + + return javaExecutablePath; + } + + private async getVersionOfJavaExecutable(executable: string, regExp: RegExp): Promise { + try { + const output = await this.childProcess.exec(`"${executable}" -version`); + return regExp.exec(`${output.stderr}${EOL}${output.stdout}`)[1]; + } catch (err) { + return null; + } + } +} diff --git a/packages/doctor/lib/winreg.ts b/packages/doctor/lib/winreg.ts new file mode 100644 index 0000000000..3c46428cc7 --- /dev/null +++ b/packages/doctor/lib/winreg.ts @@ -0,0 +1,41 @@ +import * as Registry from "winreg"; + +export class WinReg { + public registryKeys: IHiveIds = { + HKLM: { registry: Registry.HKLM }, + HKCU: { registry: Registry.HKCU }, + HKCR: { registry: Registry.HKCR }, + HKCC: { registry: Registry.HKCC }, + HKU: { registry: Registry.HKU } + }; + + public getRegistryItem(valueName: string, hive?: IHiveId, key?: string, host?: string): Promise { + return new Promise((resolve, reject) => { + const regKey = new Registry({ + hive: (hive && hive.registry) ? hive.registry : null, + key: key, + host: host + }); + + regKey.get(valueName, (err: Error, value: Registry.RegistryItem) => { + if (err) { + reject(err); + } else { + resolve(value); + } + }); + }); + } + + public getRegistryValue(valueName: string, hive?: IHiveId, key?: string, host?: string): Promise { + return new Promise((resolve, reject) => { + return this.getRegistryItem(valueName, hive, key, host) + .then((data) => { + resolve(data.value); + }) + .catch(() => { + resolve(null); + }); + }); + } +} diff --git a/packages/doctor/lib/wrappers/child-process.ts b/packages/doctor/lib/wrappers/child-process.ts new file mode 100644 index 0000000000..90366925e1 --- /dev/null +++ b/packages/doctor/lib/wrappers/child-process.ts @@ -0,0 +1,94 @@ +import * as childProcess from "child_process"; + +export class ChildProcess { + public spawnFromEvent(command: string, args: string[], event: string, options?: ISpawnFromEventOptions): Promise { + return new Promise((resolve, reject) => { + options = options || {}; + const commandChildProcess = childProcess.spawn(command, args, options.spawnOptions); + let capturedOut = ""; + let capturedErr = ""; + + if (commandChildProcess.stdout) { + commandChildProcess.stdout.on("data", (data: string) => { + capturedOut += data; + }); + } + + if (commandChildProcess.stderr) { + commandChildProcess.stderr.on("data", (data: string) => { + capturedErr += data; + }); + } + + commandChildProcess.on(event, (arg: any) => { + const exitCode = typeof arg === "number" ? arg : arg && arg.code; + const result = { + stdout: capturedOut, + stderr: capturedErr, + exitCode: exitCode + }; + + if (options.ignoreError) { + resolve(result); + } else { + if (exitCode === 0) { + resolve(result); + } else { + let errorMessage = `Command ${command} failed with exit code ${exitCode}`; + if (capturedErr) { + errorMessage += ` Error output: \n ${capturedErr}`; + } + + reject(errorMessage); + } + } + }); + + commandChildProcess.once("error", (err: Error) => { + if (options.ignoreError) { + const result = { + stdout: capturedOut, + stderr: err.message, + exitCode: (err).code + }; + resolve(result); + } else { + reject(err); + } + }); + }); + } + + public exec(command: string, options?: childProcess.ExecOptions): Promise { + return new Promise((resolve, reject) => { + childProcess.exec(command, options, (err, stdout, stderr) => { + if (err) { + reject(err); + } + + const result: IProcessInfo = { + stdout, + stderr + }; + + resolve(result); + }); + }); + } + + public execSync(command: string, options?: childProcess.ExecSyncOptions): string { + return childProcess.execSync(command, options).toString(); + } + + public execFile(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile(command, args, (error, stdout) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }); + }); + } +} diff --git a/packages/doctor/lib/wrappers/file-system.ts b/packages/doctor/lib/wrappers/file-system.ts new file mode 100644 index 0000000000..ccae7c94f3 --- /dev/null +++ b/packages/doctor/lib/wrappers/file-system.ts @@ -0,0 +1,66 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as yauzl from "yauzl"; +import * as shelljs from "shelljs"; + +export class FileSystem { + public exists(filePath: string): boolean { + return fs.existsSync(path.resolve(filePath)); + } + + public extractZip(pathToZip: string, outputDir: string): Promise { + return new Promise((resolve, reject) => { + yauzl.open(pathToZip, { autoClose: true, lazyEntries: true }, (openError, zipFile) => { + if (openError) { + return reject(openError); + } + + zipFile.on('entry', entry => { + const fn = entry.fileName; + if (/\/$/.test(fn)) { + return zipFile.readEntry(); + } + + zipFile.openReadStream(entry, (openStreamError, stream) => { + if (openStreamError) { + return reject(openStreamError); + } + + const filePath = `${outputDir}/${fn}`; + + return createParentDirsIfNeeded(filePath) + .catch(createParentDirError => reject(createParentDirError)) + .then(() => { + const outfile = fs.createWriteStream(filePath); + stream.once('end', () => { + zipFile.readEntry(); + }); + stream.pipe(outfile); + }); + }); + }); + + zipFile.once('end', () => resolve()); + + zipFile.readEntry(); + }); + }); + } + + public readDirectory(directoryPath: string): string[] { + return fs.readdirSync(directoryPath); + } + + public readJson(filePath: string, options?: { encoding?: null; flag?: string; }): T { + const content = fs.readFileSync(filePath, options); + return JSON.parse(content.toString()); + } + + public deleteEntry(filePath: string): void { + shelljs.rm("-rf", filePath); + } +} + +function createParentDirsIfNeeded(filePath: string) { + return fs.promises.mkdir(path.dirname(filePath), {recursive: true}); +} diff --git a/packages/doctor/package.json b/packages/doctor/package.json new file mode 100644 index 0000000000..5ea19f042d --- /dev/null +++ b/packages/doctor/package.json @@ -0,0 +1,64 @@ +{ + "name": "@nativescript/doctor", + "version": "2.0.6", + "description": "Library that helps identifying if the environment can be used for development of {N} apps.", + "main": "lib/index.js", + "types": "./typings/nativescript-doctor.d.ts", + "scripts": { + "clean": "npx rimraf node_modules package-lock.json && npm i && grunt clean", + "build": "grunt", + "build.all": "grunt ts:devall", + "pack": "grunt pack", + "test": "istanbul cover ./node_modules/mocha/bin/_mocha -- --recursive", + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NativeScript/nativescript-doctor.git" + }, + "keywords": [ + "NativeScript", + "doctor", + "tns" + ], + "author": "Telerik ", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/NativeScript/nativescript-doctor/issues" + }, + "homepage": "https://github.com/NativeScript/nativescript-doctor#readme", + "devDependencies": { + "@types/chai": "4.1.0", + "@types/lodash": "4.14.123", + "@types/mocha": "2.2.32", + "@types/rimraf": "2.0.2", + "@types/semver": "5.5.0", + "@types/shelljs": "0.8.6", + "@types/temp": "0.8.29", + "@types/winreg": "1.2.30", + "@types/yauzl": "2.9.0", + "chai": "4.1.2", + "conventional-changelog-cli": "^2.0.34", + "grunt": "1.0.3", + "grunt-contrib-clean": "1.0.0", + "grunt-contrib-watch": "1.1.0", + "grunt-shell": "2.0.0", + "grunt-ts": "6.0.0-beta.21", + "grunt-tslint": "5.0.2", + "istanbul": "0.4.5", + "mocha": "5.2.0", + "rimraf": "2.5.4", + "tslint": "5.14.0", + "tslint-microsoft-contrib": "6.1.0", + "typescript": "~3.8.3" + }, + "dependencies": { + "lodash": "4.17.15", + "osenv": "0.1.3", + "semver": "5.5.1", + "shelljs": "~0.8.4", + "temp": "0.8.3", + "winreg": "1.2.2", + "yauzl": "2.10.0" + } +} diff --git a/packages/doctor/resources/cocoapods-verification/cocoapods.zip b/packages/doctor/resources/cocoapods-verification/cocoapods.zip new file mode 100644 index 0000000000..2a6bad6635 Binary files /dev/null and b/packages/doctor/resources/cocoapods-verification/cocoapods.zip differ diff --git a/packages/doctor/test/android-local-build-requirements.ts b/packages/doctor/test/android-local-build-requirements.ts new file mode 100644 index 0000000000..704befb394 --- /dev/null +++ b/packages/doctor/test/android-local-build-requirements.ts @@ -0,0 +1,103 @@ +import { AndroidLocalBuildRequirements } from '../lib/local-build-requirements/android-local-build-requirements'; +import { assert } from "chai"; + +interface ITestCaseData { + testName: string; + validateInfo?: NativeScriptDoctor.IWarning[]; + validateJavacVersion?: NativeScriptDoctor.IWarning[]; + getJavaCompilerVersion?: string; + getAdbVersion?: string; +} + +describe("androidLocalBuildRequirements", () => { + describe("checkRequirements", () => { + const setupTestCase = (results: ITestCaseData): AndroidLocalBuildRequirements => { + const androidToolsInfo: NativeScriptDoctor.IAndroidToolsInfo = { + ANDROID_TARGET_PREFIX: "", + androidHome: "", + validateInfo: (): NativeScriptDoctor.IWarning[] => results.validateInfo || [], + validateAndroidHomeEnvVariable: (): NativeScriptDoctor.IWarning[] => [], + getToolsInfo: (): NativeScriptDoctor.IAndroidToolsInfoData => null, + validateJavacVersion: (installedJavaVersion: string, projectDir?: string, runtimeVersion?: string): NativeScriptDoctor.IWarning[] => results.validateJavacVersion || [], + getPathToAdbFromAndroidHome: async (): Promise => undefined, + getPathToEmulatorExecutable: (): string => undefined, + validateMinSupportedTargetSdk: (): NativeScriptDoctor.IWarning[] => [], + validataMaxSupportedTargetSdk: (): NativeScriptDoctor.IWarning[] => [] + }; + + const sysInfo: NativeScriptDoctor.ISysInfo = { + getJavaCompilerVersion: async (): Promise => results.hasOwnProperty("getJavaCompilerVersion") ? results.getJavaCompilerVersion : "8.0.0", + getJavaVersion: async (): Promise => results.hasOwnProperty("getJavaVersion") ? results.getJavaCompilerVersion : "8.0.0", + getJavaVersionFromJavaHome: async (): Promise => results.hasOwnProperty("getJavaVersionFromJavaHome") ? results.getJavaCompilerVersion : "8.0.0", + getJavaVersionFromPath: async (): Promise => results.hasOwnProperty("getJavaVersionFromPath") ? results.getJavaCompilerVersion : "8.0.0", + getJavaPath: async (): Promise => undefined, + getAdbVersion: async (pathToAdb?: string): Promise => results.hasOwnProperty("getAdbVersion") ? results.getAdbVersion : "1.0.39", + getXcodeVersion: async (): Promise => undefined, + getNodeVersion: async (): Promise => undefined, + getNpmVersion: async (): Promise => undefined, + getNodeGypVersion: async (): Promise => undefined, + getXcodeprojLocation: async (): Promise => undefined, + isITunesInstalled: async (): Promise => false, + getCocoaPodsVersion: async (): Promise => undefined, + getOs: async (): Promise => undefined, + isAndroidInstalled: async (): Promise => false, + getMonoVersion: async (): Promise => undefined, + getGitVersion: async (): Promise => undefined, + getGitPath: async (): Promise => undefined, + getGradleVersion: async (): Promise => undefined, + isCocoaPodsWorkingCorrectly: async (): Promise => false, + getNativeScriptCliVersion: async (): Promise => undefined, + getXcprojInfo: async (): Promise => null, + isCocoaPodsUpdateRequired: async (): Promise => true, + isAndroidSdkConfiguredCorrectly: async (): Promise => true, + getSysInfo: async (config?: NativeScriptDoctor.ISysInfoConfig): Promise => null, + setShouldCacheSysInfo: (shouldCache: boolean): void => undefined + }; + + const androidLocalBuildRequirements = new AndroidLocalBuildRequirements(androidToolsInfo, sysInfo); + return androidLocalBuildRequirements; + }; + + it("returns true when everything is setup correctly", async () => { + const androidLocalBuildRequirements = setupTestCase({ testName: "returns true when everything is setup correctly" }); + const result = await androidLocalBuildRequirements.checkRequirements(); + assert.isTrue(result); + }); + + describe("returns false", () => { + const getWarnings = (): NativeScriptDoctor.IWarning[] => { + return [{ + warning: "warning", + additionalInformation: "additional info", + platforms: ["android"] + }]; + }; + const testData: ITestCaseData[] = [ + { + testName: "when java is not installed", + getJavaCompilerVersion: null + }, + { + testName: "when java is installed, but it is not compatible for current project", + validateJavacVersion: getWarnings() + }, + { + testName: "when Android tools are not installed correctly", + validateInfo: getWarnings() + }, + { + testName: "when adb cannot be found", + getAdbVersion: null + } + ]; + + testData.forEach(testCase => { + it(testCase.testName, async () => { + const androidLocalBuildRequirements = setupTestCase(testCase); + const result = await androidLocalBuildRequirements.checkRequirements(); + assert.isFalse(result); + }); + }); + }); + }); +}); diff --git a/packages/doctor/test/android-tools-info.ts b/packages/doctor/test/android-tools-info.ts new file mode 100644 index 0000000000..6468a5480f --- /dev/null +++ b/packages/doctor/test/android-tools-info.ts @@ -0,0 +1,268 @@ +import { AndroidToolsInfo } from '../lib/android-tools-info'; +import { EOL } from 'os'; +import { assert } from "chai"; +import { ChildProcess } from '../lib/wrappers/child-process'; +import { FileSystem } from '../lib/wrappers/file-system'; +import { HostInfo } from '../lib/host-info'; +import { Helpers } from '../lib/helpers'; +import { Constants } from '../lib/constants'; + +interface ITestData { + javacVersion: string; + warnings?: string[]; + runtimeVersion?: string; + additionalInformation?: string; +} + +describe("androidToolsInfo", () => { + const originalAndroidHome = process.env["ANDROID_HOME"]; + const defaultAdditionalInformation = "You will not be able to build your projects for Android." + EOL + + "To be able to build for Android, verify that you have installed The Java Development Kit (JDK) and configured it according to system requirements as" + EOL + + " described in " + Constants.SYSTEM_REQUIREMENTS_LINKS[process.platform]; + before(() => { + process.env["ANDROID_HOME"] = "test"; + }); + const getAndroidToolsInfo = (runtimeVersion?: string): AndroidToolsInfo => { + const childProcess: ChildProcess = {}; + const fs: FileSystem = { + exists: () => true, + execSync: (): string => null, + readJson: (): any => { + return runtimeVersion ? { + nativescript: { + "tns-android": { + version: runtimeVersion + } + }, + devDependencies: { + "@nativescript/android": runtimeVersion + } + } : null; + }, + readDirectory: (path: string) => { + if (path.indexOf("build-tools") >= 0) { + return [ + "20.0.0", + "27.0.3", + "28.0.3", + "29.0.1" + ]; + } else { + return [ + "android-16", + "android-27", + "android-28", + "android-29" + ]; + } + } + }; + const hostInfo: HostInfo = {}; + const helpers: Helpers = new Helpers({}); + return new AndroidToolsInfo(childProcess, fs, hostInfo, helpers); + }; + + describe("getToolsInfo", () => { + it("runtime 6.0.0", () => { + const androidToolsInfo = getAndroidToolsInfo("6.0.0"); + const toolsInfo = androidToolsInfo.getToolsInfo({ projectDir: "test" }); + + assert.equal(toolsInfo.compileSdkVersion, 28); + }); + + it("runtime 6.1.0", () => { + const androidToolsInfo = getAndroidToolsInfo("6.1.0"); + const toolsInfo = androidToolsInfo.getToolsInfo({ projectDir: "test" }); + + assert.equal(toolsInfo.compileSdkVersion, 29); + }); + }); + + describe("supportedAndroidSdks", () => { + it("should support android-17 - android-31", () => { + const min = 17; + const max = 31; + let cnt = 0; + const androidToolsInfo = getAndroidToolsInfo("6.5.0"); + const supportedTargets = androidToolsInfo.getSupportedTargets("test"); + for (let i = 0; i < supportedTargets.length; i++) { + assert.equal(supportedTargets[i], `android-${min + i}`); + cnt = min + i; + } + assert.equal(cnt, max); + }); + }); + + describe("validateJavacVersion", () => { + const testData: ITestData[] = [ + { + javacVersion: "1.8.0" + }, + { + javacVersion: "1.8.0_152" + }, + { + javacVersion: "9" + }, + { + javacVersion: "9.0.1" + }, + { + javacVersion: "10" + }, + { + javacVersion: "10.0.1" + }, + { + javacVersion: "1.7.0", + warnings: [AndroidToolsInfo.unsupportedJavaMessage("1.7.0")] + }, + { + javacVersion: "1.7.0_132", + warnings: [AndroidToolsInfo.unsupportedJavaMessage("1.7.0_132")] + }, + // + // Reinstate this test if there is some future max java version found to be not supported. + // + // { + // javacVersion: "14.1.0", + // warnings: [AndroidToolsInfo.unsupportedJavaMessage("14.1.0")] + // }, + { + javacVersion: null, + warnings: ["Error executing command 'javac'. Make sure you have installed The Java Development Kit (JDK) and set JAVA_HOME environment variable."] + }, + { + javacVersion: "10", + runtimeVersion: "4.0.0", + warnings: [`The Java compiler version 10 is not compatible with the current Android runtime version 4.0.0. ` + + `In order to use this Javac version, you need to update your Android runtime or downgrade your Java compiler version.`], + additionalInformation: "You will not be able to build your projects for Android." + EOL + + "To be able to build for Android, downgrade your Java compiler version or update your Android runtime." + }, + { + javacVersion: "10", + runtimeVersion: "4.2.0" + } + ]; + + testData.forEach(({ javacVersion, warnings, runtimeVersion, additionalInformation }) => { + it(`returns correct result when version is ${javacVersion}`, () => { + const androidToolsInfo = getAndroidToolsInfo(runtimeVersion); + const actualWarnings = androidToolsInfo.validateJavacVersion(javacVersion, "/Users/username/projectDir"); + + let expectedWarnings: NativeScriptDoctor.IWarning[] = []; + if (warnings && warnings.length) { + expectedWarnings = warnings.map(warning => { + return { + platforms: [Constants.ANDROID_PLATFORM_NAME], + warning, + additionalInformation: additionalInformation || defaultAdditionalInformation + }; + }); + } + + assert.deepEqual(actualWarnings, expectedWarnings); + }); + }); + + const npmTagsTestData: ITestData[] = [ + { + javacVersion: "1.8.0", + runtimeVersion: "rc" + }, + { + javacVersion: "10", + runtimeVersion: "rc" + }, + { + javacVersion: "10", + runtimeVersion: "latest", + warnings: [`The Java compiler version 10 is not compatible with the current Android runtime version 4.0.0. ` + + `In order to use this Javac version, you need to update your Android runtime or downgrade your Java compiler version.`], + additionalInformation: "You will not be able to build your projects for Android." + EOL + + "To be able to build for Android, downgrade your Java compiler version or update your Android runtime." + }, + { + javacVersion: "10", + runtimeVersion: "old", + warnings: [`The Java compiler version 10 is not compatible with the current Android runtime version 4.1.0-2018.5.17.1. ` + + `In order to use this Javac version, you need to update your Android runtime or downgrade your Java compiler version.`], + additionalInformation: "You will not be able to build your projects for Android." + EOL + + "To be able to build for Android, downgrade your Java compiler version or update your Android runtime." + }, + { + javacVersion: "10", + runtimeVersion: "old", + warnings: [`The Java compiler version 10 is not compatible with the current Android runtime version 4.1.0-2018.5.17.1. ` + + `In order to use this Javac version, you need to update your Android runtime or downgrade your Java compiler version.`], + additionalInformation: "You will not be able to build your projects for Android." + EOL + + "To be able to build for Android, downgrade your Java compiler version or update your Android runtime." + }, + { + javacVersion: "1.8.0", + runtimeVersion: "latest" + }, + { + javacVersion: "1.8.0", + runtimeVersion: "old" + }, + ]; + + npmTagsTestData.forEach(({ javacVersion, warnings, runtimeVersion, additionalInformation }) => { + it(`returns correct result when javac version is ${javacVersion} and the runtime version is tag from npm: ${runtimeVersion}`, () => { + let execSyncCommand: string = null; + const childProcess: ChildProcess = { + execSync: (command: string) => { + execSyncCommand = command; + return JSON.stringify({ + latest: '4.0.0', + rc: '4.1.0-rc-2018.5.21.1', + next: '4.1.0-2018.5.23.2', + old: '4.1.0-2018.5.17.1' + }); + } + }; + + const fs: FileSystem = { + exists: (filePath: string): boolean => true, + execSync: (): string => null, + readJson: (): any => + ({ + nativescript: { + "tns-android": { + version: runtimeVersion + } + }, + devDependencies: { + "@nativescript/android": runtimeVersion + } + }) + }; + + const hostInfo: HostInfo = {}; + const helpers: Helpers = new Helpers({}); + const androidToolsInfo = new AndroidToolsInfo(childProcess, fs, hostInfo, helpers); + + const actualWarnings = androidToolsInfo.validateJavacVersion(javacVersion, "/Users/username/projectDir"); + let expectedWarnings: NativeScriptDoctor.IWarning[] = []; + if (warnings && warnings.length) { + expectedWarnings = warnings.map(warning => { + return { + platforms: [Constants.ANDROID_PLATFORM_NAME], + warning, + additionalInformation: additionalInformation || defaultAdditionalInformation + }; + }); + } + + assert.deepEqual(actualWarnings, expectedWarnings); + assert.equal(execSyncCommand, "npm view tns-android dist-tags --json"); + }); + }); + }); + + after(() => { + process.env["ANDROID_HOME"] = originalAndroidHome; + }); +}); diff --git a/packages/doctor/test/ios-local-build-requirements.ts b/packages/doctor/test/ios-local-build-requirements.ts new file mode 100644 index 0000000000..31ccc48016 --- /dev/null +++ b/packages/doctor/test/ios-local-build-requirements.ts @@ -0,0 +1,125 @@ +import { IosLocalBuildRequirements } from "../lib/local-build-requirements/ios-local-build-requirements"; +import { assert } from "chai"; +import { HostInfo } from "../lib/host-info"; +import { Constants } from "../lib/constants"; + +interface ITestCaseData { + testName: string; + expectedResult: boolean; + getXcodeVersion?: string; + minRequiredXcodeVersion?: number; + getXcodeprojLocation?: string; + isDarwin?: boolean; +} + +describe("iOSLocalBuildRequirements", () => { + const setupTestCase = (results: ITestCaseData): IosLocalBuildRequirements => { + const sysInfo: NativeScriptDoctor.ISysInfo = { + getXcodeVersion: async (): Promise => results.hasOwnProperty("getXcodeVersion") ? results.getXcodeVersion : "10.0", + getXcodeprojLocation: async (): Promise => results.hasOwnProperty("getXcodeprojLocation") ? results.getXcodeprojLocation : "path to xcodeproj", + }; + + const hostInfo: HostInfo = { + isDarwin: results.hasOwnProperty("isDarwin") ? results.isDarwin : true + }; + + const iOSLocalBuildRequirements = new IosLocalBuildRequirements(sysInfo, hostInfo); + return iOSLocalBuildRequirements; + }; + + describe("isXcodeVersionValid", () => { + const testCases: ITestCaseData[] = [ + { + testName: "returns false when Xcode is not installed", + getXcodeVersion: null, + expectedResult: false + }, + { + testName: "returns false when Xcode's major version is below min required version", + getXcodeVersion: "10.0", + minRequiredXcodeVersion: 11, + expectedResult: false + }, + { + testName: "returns true when Xcode's major version equals min required version", + getXcodeVersion: "10.0", + minRequiredXcodeVersion: 10, + expectedResult: true + }, + { + testName: "returns true when Xcode's major version equals min required version", + getXcodeVersion: "12.0", + minRequiredXcodeVersion: 10, + expectedResult: true + } + ]; + + testCases.forEach(testCase => { + it(testCase.testName, async () => { + const iOSLocalBuildRequirements = setupTestCase(testCase); + const originalXcodeVersion = Constants.XCODE_MIN_REQUIRED_VERSION; + Constants.XCODE_MIN_REQUIRED_VERSION = testCase.minRequiredXcodeVersion || Constants.XCODE_MIN_REQUIRED_VERSION; + + const isXcodeVersionValid = await iOSLocalBuildRequirements.isXcodeVersionValid(); + + // Get back the XCODE_MIN_REQUIRED_VERSION value. + Constants.XCODE_MIN_REQUIRED_VERSION = originalXcodeVersion; + + assert.equal(isXcodeVersionValid, testCase.expectedResult); + }); + }); + }); + + describe("checkRequirements", () => { + const testCases: ITestCaseData[] = [ + { + testName: "returns false when OS is not macOS", + isDarwin: false, + expectedResult: false + }, + { + testName: "returns false when Xcode is not installed", + getXcodeVersion: null, + expectedResult: false + }, + { + testName: "returns false when Xcode's major version is below min required version", + getXcodeVersion: "10.0", + minRequiredXcodeVersion: 11, + expectedResult: false + }, + { + testName: "returns false when xcodeproj is not installed", + getXcodeprojLocation: null, + expectedResult: false + }, + { + testName: "returns true when Xcode's major version equals min required version", + getXcodeVersion: "10.0", + minRequiredXcodeVersion: 10, + expectedResult: true + }, + { + testName: "returns true when Xcode's major version equals min required version", + getXcodeVersion: "12.0", + minRequiredXcodeVersion: 10, + expectedResult: true + } + ]; + + testCases.forEach(testCase => { + it(testCase.testName, async () => { + const iOSLocalBuildRequirements = setupTestCase(testCase); + const originalXcodeVersion = Constants.XCODE_MIN_REQUIRED_VERSION; + Constants.XCODE_MIN_REQUIRED_VERSION = testCase.minRequiredXcodeVersion || Constants.XCODE_MIN_REQUIRED_VERSION; + + const isXcodeVersionValid = await iOSLocalBuildRequirements.checkRequirements(); + + // Get back the XCODE_MIN_REQUIRED_VERSION value. + Constants.XCODE_MIN_REQUIRED_VERSION = originalXcodeVersion; + + assert.equal(isXcodeVersionValid, testCase.expectedResult); + }); + }); + }); +}); diff --git a/packages/doctor/test/sys-info.ts b/packages/doctor/test/sys-info.ts new file mode 100644 index 0000000000..15c8db2a53 --- /dev/null +++ b/packages/doctor/test/sys-info.ts @@ -0,0 +1,696 @@ +import * as assert from "assert"; +import * as path from "path"; +import { EOL } from "os"; +import { SysInfo } from "../lib/sys-info"; +import { Helpers } from "../lib/helpers"; +import { ChildProcess } from "../lib/wrappers/child-process"; + +const JavaHomeName = "JAVA_HOME"; +const AndroidHomeName = "ANDROID_HOME"; +const PROGRAM_FILES = "ProgramFiles"; +const PROGRAM_FILES_X86 = "ProgramFiles(x86)"; +const LOCAL_APP_DATA = "LOCALAPPDATA"; +const PROGRAM_FILES_ENV_PATH = "C:\\Program Files"; +const PROGRAM_FILEX_X86_ENV_PATH = "C:\\Program Files(x86)"; +const LOCAL_APP_DATA_ENV_PATH = "C:\\username\\localappdata"; + +interface IChildProcessResultDescription { + result?: any; + shouldThrowError?: boolean; + errorCode?: number; +} + +interface ICLIOutputVersionTestCase { + testedProperty: string; + method: (sysInfo: SysInfo) => Promise; +} + +interface IChildProcessResults { + [property: string]: IChildProcessResultDescription; + uname: IChildProcessResultDescription; + npmV: IChildProcessResultDescription; + nodeV: IChildProcessResultDescription; + javacVersion: IChildProcessResultDescription; + javaVersion: IChildProcessResultDescription; + nodeGypVersion: IChildProcessResultDescription; + xCodeVersion: IChildProcessResultDescription; + adbVersion: IChildProcessResultDescription; + androidInstalled: IChildProcessResultDescription; + monoVersion: IChildProcessResultDescription; + gradleVersion: IChildProcessResultDescription; + gitVersion: IChildProcessResultDescription; + podVersion: IChildProcessResultDescription; + pod: IChildProcessResultDescription; + nativeScriptCliVersion: IChildProcessResultDescription; + git: IChildProcessResultDescription; + pythonInfo?: IChildProcessResultDescription; +} + +interface IHostInfoMockOptions { + isWindows?: boolean; + dotNetVersion?: string; + isDarwin?: boolean; +} + +interface IFileSystemMockOptions { + existsResult?: boolean; +} + +const androidToolsInfo: NativeScriptDoctor.IAndroidToolsInfo = { + ANDROID_TARGET_PREFIX: "", + androidHome: "", + getPathToAdbFromAndroidHome: async () => { + return "adb"; + }, + getPathToEmulatorExecutable: () => { + return "emulator"; + }, + getToolsInfo: () => { + return Object.create(null); + }, + validateAndroidHomeEnvVariable: (): any[] => { + return []; + }, + validateInfo: (): any[] => { + return []; + }, + validateJavacVersion: (): any[] => { + return []; + }, + validateMinSupportedTargetSdk: (): NativeScriptDoctor.IWarning[] => [], + validataMaxSupportedTargetSdk: (): NativeScriptDoctor.IWarning[] => [] +}; + +function createChildProcessResults(childProcessResult: IChildProcessResults): IDictionary { + return { + "uname -a": childProcessResult.uname, + "npm -v": childProcessResult.npmV, + "node -v": childProcessResult.nodeV, + '"javac" -version': childProcessResult.javacVersion, + '"java" -version': childProcessResult.javaVersion, + 'which javac': { result: '' }, + 'where javac': { result: '' }, + 'which java': { result: '' }, + 'where java': { result: '' }, + "node-gyp -v": childProcessResult.nodeGypVersion, + "xcodebuild -version": childProcessResult.xCodeVersion, + "pod --version": childProcessResult.podVersion, + "pod": childProcessResult.pod, + "adb": childProcessResult.adbVersion, + "adb version": childProcessResult.adbVersion, + "'adb' version": childProcessResult.adbVersion, // for Mac and Linux + "android": childProcessResult.androidInstalled, + "android.bat": childProcessResult.androidInstalled, // for Windows + "mono --version": childProcessResult.monoVersion, + "'git' --version": childProcessResult.gitVersion, // for Mac and Linux + '"C:\\Program Files\\Git\\cmd\\git.exe" --version': childProcessResult.gitVersion, // for Windows + '"C:\\Program Files/Git/cmd/git.exe" --version': childProcessResult.gitVersion, // When running Windows test on the Non-Windows platform + "gradle -v": childProcessResult.gradleVersion, + "tns --version": childProcessResult.nativeScriptCliVersion, + "emulator": { shouldThrowError: false }, + "which git": childProcessResult.git, + 'python -c "import six"': childProcessResult.pythonInfo + }; +} + +function getResultFromChildProcess(childProcessResultDescription: IChildProcessResultDescription, command: string, options?: ISpawnFromEventOptions): any { + if (childProcessResultDescription.shouldThrowError) { + if (options && options.ignoreError) { + return null; + } else { + const error = new Error(`This one throws error. (${command})`); + if (childProcessResultDescription.errorCode) { + (error).code = childProcessResultDescription.errorCode; + } + + throw error; + } + } + + return childProcessResultDescription.result; +} + +function mockSysInfo(childProcessResult: IChildProcessResults, hostInfoOptions?: IHostInfoMockOptions, fileSystemOptions?: IFileSystemMockOptions): SysInfo { + hostInfoOptions = hostInfoOptions || {}; + const winreg: any = { + getRegistryValue: (valueName: string, hive?: IHiveId, key?: string, host?: string) => { return { value: "registryKey" }; }, + registryKeys: { + HKLM: "HKLM" + } + }; + const hostInfo: any = { + dotNetVersion: () => Promise.resolve(hostInfoOptions.dotNetVersion), + isDarwin: hostInfoOptions.isDarwin, + isWindows: hostInfoOptions.isWindows, + isLinux: (!hostInfoOptions.isDarwin && !hostInfoOptions.isWindows), + winreg + }; + const childProcessResultDictionary = createChildProcessResults(childProcessResult); + const childProcess = { + exec: async (command: string) => { + return getResultFromChildProcess(childProcessResultDictionary[command], command); + }, + spawnFromEvent: async (command: string, args: string[], event: string, options: ISpawnFromEventOptions) => { + return getResultFromChildProcess(childProcessResultDictionary[command], command, options); + }, + execFile: async (): Promise => { + return undefined; + }, + execSync: (command: string): string => null + }; + + const fileSystem: any = { + exists: () => (fileSystemOptions || {}).existsResult, + extractZip: () => Promise.resolve(), + readDirectory: () => Promise.resolve([]) + }; + + const helpers = new Helpers(hostInfo); + return new SysInfo(childProcess, fileSystem, helpers, hostInfo, winreg, androidToolsInfo); +} + +function setStdOut(value: string): { stdout: string } { + return { stdout: value }; +} +function setStdErr(value: string): { stderr: string } { + return { stderr: value }; +} + +describe("SysInfo unit tests", () => { + let sysInfo: SysInfo; + const dotNetVersion = "4.5.1"; + + beforeEach(() => { + // We need to mock this because on Mac the tests in which the platform is mocked to Windows in the process there will be no CommonProgramFiles. + process.env["CommonProgramFiles"] = process.env["CommonProgramFiles"] || "mocked on mac"; + process.env["CommonProgramFiles(x86)"] = process.env["CommonProgramFiles(x86)"] || "mocked on mac"; + }); + + describe("Should execute correct commands to check for", () => { + let spawnFromEventCommand: string; + let execCommands: string[] = []; + let fileSystem: any; + let hostInfo: any; + + beforeEach(() => { + execCommands = []; + const childProcess: ChildProcess = { + spawnFromEvent: async (command: string, args: string[], event: string) => { + spawnFromEventCommand = `${command} ${args.join(" ")}`; + return { stdout: "", stderr: "" }; + }, + exec: async (command: string) => { + execCommands.push(command); + return { stdout: "", stderr: "" }; + }, + execFile: async () => { + return undefined; + }, + execSync: (command: string): string => null + }; + + const helpers = new Helpers(null); + fileSystem = { + exists: () => false, + extractZip: () => Promise.resolve(), + readDirectory: () => Promise.resolve([]) + }; + + hostInfo = { + isWindows: false + }; + + sysInfo = new SysInfo(childProcess, fileSystem, helpers, hostInfo, null, androidToolsInfo); + }); + + it("java compiler version when there is JAVA_HOME.", async () => { + const originalJavaHome = process.env[JavaHomeName]; + process.env[JavaHomeName] = "mock"; + + const pathToJavac = path.join(process.env[JavaHomeName], "bin", "javac"); + fileSystem.exists = () => true; + await sysInfo.getJavaCompilerVersion(); + + process.env[JavaHomeName] = originalJavaHome; + assert.deepEqual(execCommands[0], `"${pathToJavac}" -version`); + }); + + it("java compiler version when there is no JAVA_HOME on non-Windows OS", async () => { + const originalJavaHome = process.env[JavaHomeName]; + + delete process.env[JavaHomeName]; + await sysInfo.getJavaCompilerVersion(); + + process.env[JavaHomeName] = originalJavaHome; + assert.deepEqual(execCommands, ['which javac', '"javac" -version']); + }); + + it("null when there is incorrect JAVA_HOME on non-Windows OS", async () => { + const originalJavaHome = process.env[JavaHomeName]; + process.env[JavaHomeName] = "/some/invalid/dir/name/where/java/does/not/exist"; + + const result = await sysInfo.getJavaCompilerVersion(); + + process.env[JavaHomeName] = originalJavaHome; + + assert.deepEqual(result, null); + assert.deepEqual(execCommands, []); + }); + + it("null when there is incorrect JAVA_HOME on Window OS", async () => { + const originalJavaHome = process.env[JavaHomeName]; + hostInfo.isWindows = true; + process.env[JavaHomeName] = "C:\\Program Files\\Not existing dir"; + + const result = await sysInfo.getJavaCompilerVersion(); + + process.env[JavaHomeName] = originalJavaHome; + + assert.deepEqual(result, null); + assert.deepEqual(execCommands, []); + }); + + it("java compiler version when there is no JAVA_HOME on Window OS", async () => { + const originalJavaHome = process.env[JavaHomeName]; + hostInfo.isWindows = true; + delete process.env[JavaHomeName]; + await sysInfo.getJavaCompilerVersion(); + + process.env[JavaHomeName] = originalJavaHome; + assert.deepEqual(execCommands, ['where javac', '"javac" -version']); + }); + + it("java version when there is JAVA_HOME.", async () => { + const originalJavaHome = process.env[JavaHomeName]; + process.env[JavaHomeName] = "mock"; + + const pathToJava = path.join(process.env[JavaHomeName], "bin", "java"); + fileSystem.exists = () => true; + await sysInfo.getJavaVersion(); + + process.env[JavaHomeName] = originalJavaHome; + assert.deepEqual(execCommands[0], `"${pathToJava}" -version`); + }); + + it("java version when there is no JAVA_HOME on non-Windows OS", async () => { + const originalJavaHome = process.env[JavaHomeName]; + + delete process.env[JavaHomeName]; + await sysInfo.getJavaVersion(); + + process.env[JavaHomeName] = originalJavaHome; + assert.deepEqual(execCommands, ['which java', '"java" -version']); + }); + + it("java version when there is no JAVA_HOME on Window OS", async () => { + const originalJavaHome = process.env[JavaHomeName]; + hostInfo.isWindows = true; + delete process.env[JavaHomeName]; + await sysInfo.getJavaVersion(); + + process.env[JavaHomeName] = originalJavaHome; + assert.deepEqual(execCommands, ['where java', '"java" -version']); + }); + }); + + describe("getSysInfo", () => { + let childProcessResult: IChildProcessResults; + const originalJavaHome = process.env[JavaHomeName]; + const originalAndroidHome = process.env[AndroidHomeName]; + + beforeEach(() => { + childProcessResult = { + uname: { result: setStdOut("name") }, + npmV: { result: setStdOut("2.14.1") }, + nodeV: { result: setStdOut("v6.0.0") }, + javacVersion: { result: setStdErr("javac 1.8.0_60") }, + javaVersion: { + result: setStdErr(`java version "1.8.0_202" +Java(TM) SE Runtime Environment (build 1.8.0_202-b08) +Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)`) + }, + nodeGypVersion: { result: setStdOut("2.0.0") }, + xCodeVersion: { result: setStdOut("Xcode 6.4.0") }, + adbVersion: { result: setStdOut("Android Debug Bridge version 1.0.32") }, + androidInstalled: { result: setStdOut("android") }, + monoVersion: { result: setStdOut("version 1.0.6 ") }, + gradleVersion: { result: setStdOut("Gradle 2.8") }, + gitVersion: { result: setStdOut("git version 1.9.5") }, + podVersion: { result: setStdOut("0.38.2") }, + pod: { result: setStdOut("success") }, + nativeScriptCliVersion: { result: setStdOut("2.5.0") }, + git: { result: setStdOut("git") } + }; + + delete process.env[JavaHomeName]; + delete process.env[AndroidHomeName]; + }); + + afterEach(() => { + process.env[JavaHomeName] = originalJavaHome; + process.env[AndroidHomeName] = originalAndroidHome; + }); + + describe("returns correct results when everything is installed", () => { + const assertCommonValues = (result: NativeScriptDoctor.ISysInfoData) => { + assert.deepEqual(result.npmVer, childProcessResult.npmV.result.stdout); + assert.deepEqual(result.nodeVer, "6.0.0"); + assert.deepEqual(result.javacVersion, "1.8.0_60"); + assert.deepEqual(result.javaVersion, "1.8.0_202"); + assert.deepEqual(result.nodeGypVer, childProcessResult.nodeGypVersion.result.stdout); + assert.deepEqual(result.adbVer, "1.0.32"); + assert.deepEqual(result.androidInstalled, true); + assert.deepEqual(result.monoVer, "1.0.6"); + assert.deepEqual(result.gradleVer, "2.8"); + assert.deepEqual(result.gitVer, "1.9.5"); + assert.deepEqual(result.nativeScriptCliVersion, childProcessResult.nativeScriptCliVersion.result.stdout); + }; + + beforeEach(() => { + androidToolsInfo.validateAndroidHomeEnvVariable = (): any[] => []; + }); + + it("on Windows", async () => { + const originalProgramFiles = process.env[PROGRAM_FILES]; + process.env[PROGRAM_FILES] = PROGRAM_FILES_ENV_PATH; + process.env[PROGRAM_FILES_X86] = PROGRAM_FILEX_X86_ENV_PATH; + process.env[LOCAL_APP_DATA] = LOCAL_APP_DATA_ENV_PATH; + sysInfo = mockSysInfo(childProcessResult, { isWindows: true, isDarwin: false, dotNetVersion }, { existsResult: true }); + const result = await sysInfo.getSysInfo(); + process.env[PROGRAM_FILES] = originalProgramFiles; + assertCommonValues(result); + assert.deepEqual(result.xcodeVer, null); + assert.deepEqual(result.cocoaPodsVer, null); + }); + + it("on Mac", async () => { + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await sysInfo.getSysInfo(); + assertCommonValues(result); + assert.deepEqual(result.xcodeVer, "6.4.0"); + assert.deepEqual(result.cocoaPodsVer, childProcessResult.podVersion.result.stdout); + }); + + it("on Linux", async () => { + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: false, dotNetVersion }); + const result = await sysInfo.getSysInfo(); + assertCommonValues(result); + assert.deepEqual(result.xcodeVer, null); + assert.deepEqual(result.cocoaPodsVer, null); + }); + }); + + describe("cocoapods version", () => { + it("is null when cocoapods are not installed", async () => { + // simulate error when pod --version command is executed + childProcessResult.podVersion = { shouldThrowError: true }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await sysInfo.getSysInfo(); + assert.deepEqual(result.cocoaPodsVer, null); + }); + + it("is null when OS is not Mac", async () => { + const originalProgramFiles = process.env[PROGRAM_FILES]; + process.env[PROGRAM_FILES] = PROGRAM_FILES_ENV_PATH; + sysInfo = mockSysInfo(childProcessResult, { isWindows: true, isDarwin: false, dotNetVersion }); + const result = await sysInfo.getSysInfo(); + process.env[PROGRAM_FILES] = originalProgramFiles; + assert.deepEqual(result.cocoaPodsVer, null); + }); + + it("is correct when cocoapods output has warning after version output", async () => { + childProcessResult.podVersion = { result: setStdOut("0.38.2\nWARNING:\n") }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await sysInfo.getSysInfo(); + assert.deepEqual(result.cocoaPodsVer, "0.38.2"); + }); + + it("is correct when cocoapods output has warnings before version output", async () => { + childProcessResult.podVersion = { result: setStdOut("WARNING\nWARNING2\n0.38.2") }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await sysInfo.getSysInfo(); + assert.deepEqual(result.cocoaPodsVer, "0.38.2"); + }); + }); + + describe("getXcprojInfo", () => { + it("does not fail when cocoapods version is below 1.0.0 and Xcode version contains only two digits", async () => { + childProcessResult.podVersion = { result: setStdOut("0.39.0") }; + childProcessResult.xCodeVersion = { result: setStdOut("Xcode 8.3") }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await sysInfo.getXcprojInfo(); + assert.deepEqual(result, { shouldUseXcproj: true, xcprojAvailable: false }); + }); + }); + + describe("when android info is incorrect", () => { + it("pathToAdb is null", async () => { + childProcessResult.adbVersion = { + result: null + }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: false, dotNetVersion: "4.5.1" }, null); + const adbVersion = await sysInfo.getAdbVersion(); + const isAndroidSdkConfiguredCorrectly = await sysInfo.isAndroidSdkConfiguredCorrectly(); + assert.deepEqual(adbVersion, null); + assert.deepEqual(isAndroidSdkConfiguredCorrectly, undefined); + }); + }); + + describe("pythonInfo", () => { + it("should return null when platform is windows", async () => { + sysInfo = mockSysInfo(childProcessResult, { isWindows: true, isDarwin: false, dotNetVersion: "4.5.1" }, null); + const pythonInfo = await sysInfo.getPythonInfo(); + assert.deepEqual(pythonInfo, null); + }); + it("should return null when platform is linux", async () => { + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: false, dotNetVersion: "4.5.1" }, null); + const pythonInfo = await sysInfo.getPythonInfo(); + assert.deepEqual(pythonInfo, null); + }); + it("should return {isInstalled: true, isSixPackageInstalled: true} when python is correctly installed on Mac", async () => { + childProcessResult.pythonInfo = { result: "" }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion: "4.5.1" }, null); + const pythonInfo = await sysInfo.getPythonInfo(); + assert.deepEqual(pythonInfo, { isInstalled: true, isSixPackageInstalled: true }); + }); + it("should return {isInstalled: false, isSixPackageInstalled: false} when python check throws an error", async () => { + childProcessResult.pythonInfo = { shouldThrowError: true }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion: "4.5.1" }, null); + const pythonInfo = await sysInfo.getPythonInfo(); + assert.deepEqual(pythonInfo, { isInstalled: false, isSixPackageInstalled: false, installationErrorMessage: "This one throws error. (python -c \"import six\")" }); + }); + it("should return {isInstalled: true, isSixPackageInstalled: false} when python is installed but six package is not", async () => { + childProcessResult.pythonInfo = { shouldThrowError: true, errorCode: 1 }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion: "4.5.1" }, null); + const pythonInfo = await sysInfo.getPythonInfo(); + assert.deepEqual(pythonInfo, { isInstalled: true, isSixPackageInstalled: false }); + }); + }); + + const testData: ICLIOutputVersionTestCase[] = [ + { + testedProperty: "nativeScriptCliVersion", + method: (currentSysInfo: SysInfo) => currentSysInfo.getNativeScriptCliVersion() + }]; + + testData.forEach((testCase) => { + describe(testCase.testedProperty, () => { + it("is null when tns is not installed", async () => { + childProcessResult[testCase.testedProperty] = { shouldThrowError: true }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await testCase.method(sysInfo); + assert.deepEqual(result, null); + }); + + it("is correct when the version is the only row in command output", async () => { + childProcessResult[testCase.testedProperty] = { result: setStdOut("3.0.0") }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await testCase.method(sysInfo); + assert.deepEqual(result, "3.0.0"); + }); + + it("is correct when there are warnings in the command's output", async () => { + childProcessResult[testCase.testedProperty] = { result: setStdOut(`Some warning due to invalid extensions${EOL}3.0.0`) }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await testCase.method(sysInfo); + assert.deepEqual(result, "3.0.0"); + }); + + it("is correct when there are warnings with version in them in the command's output", async () => { + const cliOutput = ` +Support for Node.js 7.6.0 is not verified. This CLI might not install or run properly. + +3.0.0`; + childProcessResult[testCase.testedProperty] = { result: setStdOut(cliOutput) }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await testCase.method(sysInfo); + assert.deepEqual(result, "3.0.0"); + }); + + it("is correct when there are warnings in the command's output and searched version is a prerelease", async () => { + const expectedCliVersion = "3.2.0-2017-07-21-9480"; + const cliOutput = ` +Support for Node.js 7.6.0 is not verified. This CLI might not install or run properly. + +${expectedCliVersion}`; + childProcessResult[testCase.testedProperty] = { result: setStdOut(cliOutput) }; + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await testCase.method(sysInfo); + assert.deepEqual(result, expectedCliVersion); + }); + }); + }); + + describe("returns correct results when exceptions are raised during sysInfo data collection", () => { + beforeEach(() => { + childProcessResult = { + uname: { shouldThrowError: true }, + npmV: { shouldThrowError: true }, + nodeV: { shouldThrowError: true }, + javacVersion: { shouldThrowError: true }, + javaVersion: { shouldThrowError: true }, + nodeGypVersion: { shouldThrowError: true }, + xCodeVersion: { shouldThrowError: true }, + adbVersion: { shouldThrowError: true }, + androidInstalled: { shouldThrowError: true }, + monoVersion: { shouldThrowError: true }, + gradleVersion: { shouldThrowError: true }, + gitVersion: { shouldThrowError: true }, + podVersion: { shouldThrowError: true }, + pod: { shouldThrowError: true }, + nativeScriptCliVersion: { shouldThrowError: true }, + git: { shouldThrowError: false } + }; + androidToolsInfo.validateAndroidHomeEnvVariable = (): any[] => [1]; + }); + + describe("when all of calls throw", () => { + const assertAllValuesAreNull = async () => { + const result = await sysInfo.getSysInfo(); + assert.deepEqual(result.npmVer, null); + assert.deepEqual(result.javacVersion, null); + assert.deepEqual(result.nodeGypVer, null); + assert.deepEqual(result.xcodeVer, null); + assert.deepEqual(result.adbVer, null); + assert.deepEqual(result.androidInstalled, false); + assert.deepEqual(result.monoVer, null); + assert.deepEqual(result.gradleVer, null); + assert.deepEqual(result.gitVer, null); + assert.deepEqual(result.cocoaPodsVer, null); + }; + + it("on Windows", async () => { + const originalProgramFiles = process.env[PROGRAM_FILES]; + process.env[PROGRAM_FILES] = PROGRAM_FILES_ENV_PATH; + sysInfo = mockSysInfo(childProcessResult, { isWindows: true, isDarwin: false, dotNetVersion }); + process.env[PROGRAM_FILES] = originalProgramFiles; + await assertAllValuesAreNull(); + }); + + it("on Mac", async () => { + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + await assertAllValuesAreNull(); + }); + + it("on Linux", async () => { + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: false, dotNetVersion }); + await assertAllValuesAreNull(); + }); + }); + }); + + describe("returns correct sysInfo when", () => { + const assertCommonSysInfo = (result: NativeScriptDoctor.ISysInfoData) => { + assert.deepEqual(result.npmVer, childProcessResult.npmV.result.stdout); + assert.deepEqual(result.nodeVer, "6.0.0"); + assert.deepEqual(result.nodeGypVer, childProcessResult.nodeGypVersion.result.stdout); + assert.deepEqual(result.gitVer, "1.9.5"); + assert.deepEqual(result.nativeScriptCliVersion, childProcessResult.nativeScriptCliVersion.result.stdout); + }; + + const assertAndroidSysInfo = (result: NativeScriptDoctor.IAndroidSysInfoData) => { + assert.deepEqual(result.adbVer, "1.0.32"); + assert.deepEqual(result.androidInstalled, false); + assert.deepEqual(result.monoVer, "1.0.6"); + assert.deepEqual(result.gradleVer, "2.8"); + assert.deepEqual(result.javacVersion, "1.8.0_60"); + assert.deepEqual(result.isAndroidSdkConfiguredCorrectly, undefined); + }; + + const assertiOSSysInfo = (result: NativeScriptDoctor.IiOSSysInfoData) => { + assert.deepEqual(result.xcodeVer, "6.4.0"); + assert.deepEqual(result.itunesInstalled, undefined); + assert.deepEqual(result.cocoaPodsVer, "0.38.2"); + assert.deepEqual(result.xcodeprojLocation, null); + assert.deepEqual(result.isCocoaPodsWorkingCorrectly, true); + assert.deepEqual(result.xcprojInfo, undefined); + assert.deepEqual(result.isCocoaPodsUpdateRequired, false); + assert.deepEqual(result.pythonInfo, { isInstalled: false, isSixPackageInstalled: false, installationErrorMessage: "Cannot read property 'shouldThrowError' of undefined" }); + }; + + it("iOS platform is specified", async () => { + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await sysInfo.getSysInfo({ platform: "iOS" }); + + assertCommonSysInfo(result); + assertiOSSysInfo(result); + // Android specific properties should be undefined + assert.deepEqual(result.adbVer, undefined); + assert.deepEqual(result.androidInstalled, undefined); + assert.deepEqual(result.monoVer, undefined); + assert.deepEqual(result.gradleVer, undefined); + assert.deepEqual(result.javacVersion, undefined); + assert.deepEqual(result.isAndroidSdkConfiguredCorrectly, undefined); + }); + it("Android platform is specified", async () => { + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }, { existsResult: true }); + const result = await sysInfo.getSysInfo({ platform: "Android" }); + + assertCommonSysInfo(result); + assertAndroidSysInfo(result); + // iOS specific properties should be undefined + assert.deepEqual(result.xcodeVer, undefined); + assert.deepEqual(result.itunesInstalled, undefined); + assert.deepEqual(result.cocoaPodsVer, undefined); + assert.deepEqual(result.xcodeprojLocation, undefined); + assert.deepEqual(result.isCocoaPodsWorkingCorrectly, undefined); + assert.deepEqual(result.xcprojInfo, undefined); + assert.deepEqual(result.isCocoaPodsUpdateRequired, undefined); + assert.deepEqual(result.pythonInfo, undefined); + }); + it("no platform is specified", async () => { + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await sysInfo.getSysInfo(); + assertCommonSysInfo(result); + assertAndroidSysInfo(result); + assertiOSSysInfo(result); + }); + }); + + describe("getJavaVersion", () => { + it("parses correctly OpenJDK output", async () => { + childProcessResult.javaVersion = { + result: setStdOut(`openjdk version "1.8.0_64" +OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_64-b08) +OpenJDK 64-Bit Server VM (build 25.202-b08, mixed mode)`) + }; + + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await sysInfo.getJavaVersion(); + assert.equal(result, "1.8.0_64"); + }); + + it("parses correctly OpenJDK output", async () => { + childProcessResult.javaVersion = { + result: setStdOut(`java version "1.8.0_25" +Java(TM) SE Runtime Environment (build 1.8.0_25-b08) +Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)`) + }; + + sysInfo = mockSysInfo(childProcessResult, { isWindows: false, isDarwin: true, dotNetVersion }); + const result = await sysInfo.getJavaVersion(); + assert.equal(result, "1.8.0_25"); + }); + }); + }); +}); diff --git a/packages/doctor/test/wrappers/example.zip b/packages/doctor/test/wrappers/example.zip new file mode 100644 index 0000000000..9566af9710 Binary files /dev/null and b/packages/doctor/test/wrappers/example.zip differ diff --git a/packages/doctor/test/wrappers/file-system.ts b/packages/doctor/test/wrappers/file-system.ts new file mode 100644 index 0000000000..e2b0708439 --- /dev/null +++ b/packages/doctor/test/wrappers/file-system.ts @@ -0,0 +1,47 @@ +import { tmpdir } from "os"; +import { assert } from "chai"; +import * as rimraf from "rimraf"; + +import { FileSystem } from "../../lib/wrappers/file-system"; + +describe('FileSystem', () => { + describe('extractZip', () => { + const d = new Date(); + const datetime = [ + d.getFullYear(), + d.getMonth() + 1, + d.getDate(), + d.getHours(), + d.getMinutes(), + d.getSeconds(), + d.getMilliseconds() + ].join(''); + const tmpDir = `${tmpdir()}/${datetime}`; + const testFilePath = `${__dirname}/example.zip`; + const filesThatNeedToExist = [ + `${tmpDir}/test/android-local-build-requirements.ts`, + `${tmpDir}/test/android-tools-info.ts`, + `${tmpDir}/test/ios-local-build-requirements.ts`, + `${tmpDir}/test/sys-info.ts`, + `${tmpDir}/test/wrappers/file-system.ts` + ]; + + it('should extract in example zip archive in tmp folder', done => { + const fs = new FileSystem(); + + fs.extractZip(testFilePath, tmpDir) + .then(() => { + const allExists = filesThatNeedToExist + .map(fs.exists) + .reduce((acc, r) => acc && r, true); + + assert.isTrue(allExists); + + done(); + }) + .catch(e => done(e)); + }); + + afterEach(done => rimraf(tmpDir, done)); + }); +}); diff --git a/packages/doctor/tsconfig.json b/packages/doctor/tsconfig.json new file mode 100644 index 0000000000..e1ddc075df --- /dev/null +++ b/packages/doctor/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "sourceMap": true, + "declaration": false, + "removeComments": false, + "noImplicitAny": true, + "experimentalDecorators": true + }, + "exclude": [ + "node_modules", + "coverage" + ] +} diff --git a/packages/doctor/tslint.json b/packages/doctor/tslint.json new file mode 100644 index 0000000000..5386dd58d9 --- /dev/null +++ b/packages/doctor/tslint.json @@ -0,0 +1,73 @@ +{ + "rulesDirectory": "node_modules/tslint-microsoft-contrib", + "rules": { + "class-name": true, + "curly": true, + "eofline": true, + "mocha-avoid-only": true, + "indent": [ + true, + "tabs" + ], + "interface-name": true, + "jsdoc-format": true, + "max-line-length": [ + false, + 140 + ], + "prefer-const": true, + "no-consecutive-blank-lines": true, + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-shadowed-variable": true, + "no-empty": true, + "no-eval": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "no-var-requires": false, + "one-line": [ + true, + "check-catch", + "check-finally", + "check-else", + "check-open-brace", + "check-whitespace" + ], + "no-floating-promises": true, + "quotemark": [ + false, + "double" + ], + "semicolon": true, + "space-before-function-paren": false, + "switch-default": false, + "trailing-comma": [ + false, + { + "multiline": "always", + "singleline": "always" + } + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "use-isnan": true, + "variable-name": [ + true, + "ban-keywords", + "allow-leading-underscore" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-module", + "check-separator" + ] + } +} \ No newline at end of file diff --git a/packages/doctor/typings/interfaces.ts b/packages/doctor/typings/interfaces.ts new file mode 100644 index 0000000000..867431c6d7 --- /dev/null +++ b/packages/doctor/typings/interfaces.ts @@ -0,0 +1,563 @@ +declare module NativeScriptDoctor { + /** + * Describes methods which helps collecting system information. + */ + interface ISysInfo { + /** + * Returns the currently installed Java compiler version. + * @return {Promise} The currently installed Java compiler version. + */ + getJavaCompilerVersion(): Promise; + + /** + * Returns the currently installed Java version. + * @return {Promise} The currently installed Java version. + */ + getJavaVersion(): Promise; + + /** + * Returns the currently installed Java path based on JAVA_HOME and PATH.. + * @return {Promise} The currently installed Java path. + */ + getJavaPath(): Promise; + + /** + * Gets JAVA version based on the executable in PATH. + * @return {Promise} + */ + getJavaVersionFromPath(): Promise; + + /** + * Gets JAVA version based on the JAVA from JAVA_HOME. + * @return {Promise} + */ + getJavaVersionFromJavaHome(): Promise; + + /** + * Returns the currently installed version of Xcode. + * @return {Promise} Returns the currently installed version of Xcode or null if Xcode is not installed or executed on Linux or Windows. + */ + getXcodeVersion(): Promise; + + /** + * Returns the currently installed Node.js version. + * @return {Promise} Returns the currently installed Node.js version. + */ + getNodeVersion(): Promise; + + /** + * Returns the currently installed Npm version. + * @return {Promise} Returns the currently installed npm version. + */ + getNpmVersion(): Promise; + + /** + * Returns the currently installed node-gyp version. + * @return {Promise} Returns the currently installed node-gyp version. If node-gyp is not installed it will return null. + */ + getNodeGypVersion(): Promise; + + /** + * Returns the xcodeproj location. + * @return {Promise} Returns the xcodeproj location. If the the xcodeproj is not installed it will return null. + */ + getXcodeprojLocation(): Promise; + + /** + * Checks if iTunes is installed. + * @return {Promise} Returns true if iTunes is installed. + */ + isITunesInstalled(): Promise; + + /** + * Returns the currently installed Cocoapods version. + * @return {Promise} Returns the currently installed Cocoapods version. It will return null if Cocoapods is not installed. + */ + getCocoaPodsVersion(): Promise; + + /** + * Returns the os name. + * @return {Promise} Returns the os name. + */ + getOs(): Promise; + + /** + * Returns the currently installed ADB version. + * @param {string} pathToAdb Defines path to adb + * @return {Promise} Returns the currently installed ADB version. It will return null if ADB is not installed. + */ + getAdbVersion(pathToAdb?: string): Promise; + + /** + * Checks if Android is installed. + * @return {Promise} Returns true if Android is installed. + */ + isAndroidInstalled(): Promise; + + /** + * Returns the currently installed Mono version. + * @return {Promise} Returns the currently installed Mono version. It will return null if Mono is not installed. + */ + getMonoVersion(): Promise; + + /** + * Returns the currently installed Git version. + * @return {Promise} Returns the currently installed Git version. It will return null if Git is not installed. + */ + getGitVersion(): Promise; + + /** + * Returns the path to the currently installed Git. + * @return {Promise} Returns the path to the currently installed Git. It will return null if Git is not installed. + */ + getGitPath(): Promise; + + /** + * Returns the currently installed Gradle version. + * @return {Promise} Returns the currently installed Gradle version. It will return null if Gradle is not installed. + */ + getGradleVersion(): Promise; + + /** + * Checks if CocoaPods is working correctly by trying to install one pod. + * @return {Promise} Returns true if CocoaPods is working correctly. + */ + isCocoaPodsWorkingCorrectly(): Promise; + + /** + * Returns the version of the globally installed NativeScript CLI. + * @return {Promise} Returns the version of the globally installed NativeScript CLI. + */ + getNativeScriptCliVersion(): Promise; + + /** + * Checks if xcproj is required to build projects and if it is installed. + * @return {Promise} Returns the collected information aboud xcproj. + */ + getXcprojInfo(): Promise; + + /** + * Checks if the current version of CocoaPods is compatible with the installed Xcode. + * @return {Promise} true if an update us require. + */ + isCocoaPodsUpdateRequired(): Promise; + + /** + * Checks if the Android SDK Tools are installed and configured correctly. + * @return {Promise} true if the Android SDK Tools are installed and configured correctly. + */ + isAndroidSdkConfiguredCorrectly(): Promise; + + /** + * Returns the whole system information. + * @param {ISysInfoConfig} config + * @return {Promise} The system information. + */ + getSysInfo(config?: ISysInfoConfig): Promise; + + /** + * If set to true each method will cache it's result. The default value is true. + * @param {boolean} shouldCache The cache switch. + * @return {void} + */ + setShouldCacheSysInfo(shouldCache: boolean): void; + } + + interface ISysInfoConfig { + /** + * The platform for which to check if environment is properly configured. + */ + platform?: string; + /** + * Path to package.json file of NativeScript CLI + * @type {string} + */ + pathToNativeScriptCliPackageJson?: string; + /** + * Android tools data + * @type {{pathToAdb: string}} + */ + androidToolsInfo?: { + /** + * Path to adb + * @type {string} + */ + pathToAdb: string; + }; + + /** + * The project directory. Used to determine the Android Runtime version and validate the Java compiler version against it. + * If it is not passed or the project does not have Android runtime, this validation is skipped. + */ + projectDir?: string; + + /** + * The runtime version against which the validation is executed. In case this parameter is passed, it takes precedence over the projectDir argument. + */ + androidRuntimeVersion?: string; + } + + /** + * Describes methods which help identifying if the environment can be used for development of {N} apps. + */ + interface IDoctor { + /** + * Checks if a local build can be executed on the current machine. + * @param {string} platform The platform for which to check if local build is possible. + * @param {string} projectDir @optional The project directory. Used to determine the Android Runtime version and validate the Java compiler version against it. + * If it is not passed or the project does not have Android runtime, this validation is skipped. + * @param {string} runtimeVersion @optional The runtime version against which the validation is executed. In case this parameter is passed, it takes precedence over the projectDir argument. + * @return {Promise} true if local build can be executed for the provided platform. + */ + canExecuteLocalBuild(platform: string, projectDir?: string, runtimeVersion?: string): Promise; + + /** + * Executes all checks for the current environment and returns the warnings from each check. + * @return {Promise} Array of all the warnings from all checks. If there are no warnings will return empty array. + */ + getWarnings(): Promise; + + /** + * Executes all checks for the current environment and returns the info from each check + * @param {NativeScriptDoctor.ISysInfoConfig} + * @return {Promise} Array of all the infos from all checks. + */ + getInfos(config?: NativeScriptDoctor.ISysInfoConfig): Promise; + } + + interface ICommonSysInfoData { + /** + * Os platform flavour, reported by os.platform. + * @type {string} + */ + platform: string; + /** + * Full os name, like `uname -a` on unix, registry query on win. + * @type {string} + */ + os: string; + /** + * The command shell in use, usually bash or cmd. + * @type {string} + */ + shell: string; + /** + * node.js version, returned by node -v. + * @type {string} + */ + nodeVer: string; + /** + * npm version, returned by `npm -v`. + * @type {string} + */ + npmVer: string; + /** + * Process architecture, returned by `process.arch`. + * @type {string} + */ + procArch: string; + /** + * node-gyp version as returned by `node-gyp -v`. + * @type {string} + */ + nodeGypVer: string; + /** + * git version string, as returned by `git --version`. + * @type {string} + */ + gitVer: string; + /** + * NativeScript CLI version string, as returned by `tns --version`. + * @type {string} + */ + nativeScriptCliVersion: string; + } + + interface IiOSSysInfoData { + /** + * Xcode version string as returned by `xcodebuild -version`. Valid only on Mac. + * @type {string} + */ + xcodeVer: string; + /** + * Whether iTunes is installed on the machine. + * @type {boolean} + */ + itunesInstalled: boolean; + /** + * pod version string, as returned by `pod --version`. + * @type {string} + */ + cocoaPodsVer: string; + /** + * xcodeproj location, as returned by `which xcodeproj`. + * @type {string} + */ + xcodeprojLocation: string; + /** + * true id CocoaPods can successfully execute pod install. + * @type {boolean} + */ + isCocoaPodsWorkingCorrectly: boolean; + /** + * Information about xcproj. + * @type {string} + */ + xcprojInfo: IXcprojInfo; + /** + * true if the system requires xcproj to build projects successfully and the CocoaPods version is not compatible with the Xcode. + * @type {boolean} + */ + isCocoaPodsUpdateRequired: boolean; + /** + * Information about python installation + */ + pythonInfo: IPythonInfo; + } + + interface IAndroidSysInfoData { + /** + * Version string of adb, as returned by `adb version`. + * @type {string} + */ + adbVer: string; + /** + * Whether `android` executable can be run. + * @type {boolean} + */ + androidInstalled: boolean; + /** + * mono version, relevant on Mac only. + * @type {string} + */ + monoVer: string; + /** + * gradle version string as returned by `gradle -v`. + * @type {string} + */ + gradleVer: string; + /** + * javac version string as returned by `javac -version`. + * @type {string} + */ + javacVersion: string; + /** + * java version string as returned by `java -version`. + * @type {string} + */ + javaVersion: string; + /** + * java path based on JAVA_HOME and PATH. + * @type {string} + */ + javaPath: string; + /** + * true if the Android SDK Tools are installed and configured correctly. + * @type {boolean} + */ + isAndroidSdkConfiguredCorrectly: boolean; + /** + * .net version, applicable to windows only. + * @type {string} + */ + dotNetVer?: string; + } + + interface ISysInfoData extends ICommonSysInfoData, IiOSSysInfoData, IAndroidSysInfoData { } + + /** + * Describes warning returned from @nativescript/doctor check. + */ + interface IWarning { + /** + * The warning. + * @type {string} + */ + warning: string; + + /** + * Additional information for the warning. + * @type {string} + */ + additionalInformation: string; + + /** + * The platforms which are affected by this warning. + * @type {string[]} + */ + platforms: string[]; + } + + interface IInfo { + /** + * The message. + * @type {string} + */ + message: string; + + /** + * Additional information for the warning. + * @type {string} + */ + additionalInformation?: string; + + /** + * The platforms which are affected by this warning. + * @type {string[]} + */ + platforms: string[]; + + /** + * The warning. + * @type {string} // can be warning, note or info + */ + type: string; + } + + /** + * Describes information about xcproj brew formula. + */ + interface IXcprojInfo { + /** + * Determines whether the system needs xcproj to execute ios builds sucessfully. + */ + shouldUseXcproj: boolean; + + /** + * Determines whether xcproj can be called from the command line. + */ + xcprojAvailable: boolean; + } + + /** + * Describes information about python + */ + interface IPythonInfo { + /** + * Determines whether python is installed on the host machine + */ + isInstalled: boolean; + + /** + * Determines whether python six package is installed + */ + isSixPackageInstalled: boolean; + + /** + * Error message from installation check + */ + installationErrorMessage?: string; + } + + /** + * Describes the constants used in the module. + */ + interface IConstants { + ANDROID_PLATFORM_NAME: string; + IOS_PLATFORM_NAME: string; + SUPPORTED_PLATFORMS: string[]; + INFO_TYPE_NAME: string; + WARNING_TYPE_NAME: string; + } + + /** + * Describes methods for getting and validating Android tools. + */ + interface IAndroidToolsInfo { + ANDROID_TARGET_PREFIX: string; + androidHome: string; + + /** + * Returns the Android tools info. + * @return {NativeScriptDoctor.IAndroidToolsInfoData} returns the Android tools info. + */ + getToolsInfo(config: IProjectDir): NativeScriptDoctor.IAndroidToolsInfoData; + + /** + * Checks if the Android tools are valid. + * @return {NativeScriptDoctor.IWarning[]} An array of errors from the validation checks. If there are no errors will return []. + */ + validateInfo(config: IProjectDir): NativeScriptDoctor.IWarning[]; + + /** + * Checks if the current javac version is valid. + * @param {string} installedJavaVersion The version of javac to check. + * @param {string} projectDir @optional The project directory. Used to determine the Android Runtime version and validate the Java compiler version against it. + * If it is not passed or the project does not have Android runtime, this validation is skipped. + * @param {string} runtimeVersion @optional The runtime version against which the validation is executed. In case this parameter is passed, it takes precedence over the projectDir argument. + * @return {NativeScriptDoctor.IWarning[]} An array of errors from the validation checks. If there are no errors will return []. + */ + validateJavacVersion(installedJavaVersion: string, projectDir?: string, runtimeVersion?: string): NativeScriptDoctor.IWarning[]; + + /** + * Returns the path to the adb which is located in ANDROID_HOME. + * @return {Promise} Path to the adb which is located in ANDROID_HOME. + */ + getPathToAdbFromAndroidHome(): Promise; + + /** + * Checks if the ANDROID_HOME variable is set to the correct folder. + * @return {NativeScriptDoctor.IWarning[]} An array of errors from the validation checks. If there are no errors will return []. + */ + validateAndroidHomeEnvVariable(): NativeScriptDoctor.IWarning[]; + + /** + * Validates if the provided targetSdk is greater that the minimum supported target SDK. + * @param {ITargetValidationOptions} options The targetSdk to be validated and the project directory - used to determine the Android Runtime version. + * @return {NativeScriptDoctor.IWarning[]} An array of errors from the validation checks. If there are no errors will return []. + */ + validateMinSupportedTargetSdk(options: ITargetValidationOptions): NativeScriptDoctor.IWarning[]; + + /** + * Validates if the provided targetSdk is lower that the maximum supported target SDK. + * @param {ITargetValidationOptions} options The targetSdk to be validated and the project directory - used to determine the Android Runtime version. + * @return {NativeScriptDoctor.IWarning[]} An array of errors from the validation checks. If there are no errors will return []. + */ + validataMaxSupportedTargetSdk(options: ITargetValidationOptions): NativeScriptDoctor.IWarning[]; + + /** + * Returns the path to the emulator executable. + * @return {string} The path to the emulator executable. + */ + getPathToEmulatorExecutable(): string; + } + + /** + * The targetSdk to be validated. + */ + interface ITargetValidationOptions extends Partial { + targetSdk: number; + } + + /** + * Describes information about installed Android tools and SDKs. + */ + interface IAndroidToolsInfoData { + /** + * The value of ANDROID_HOME environment variable. + */ + androidHomeEnvVar: string; + + /** + * The latest installed version of Android Build Tools that satisfies CLI's requirements. + */ + buildToolsVersion: string; + + /** + * The latest installed version of Android SDK that satisfies CLI's requirements. + */ + compileSdkVersion: number; + + /** + * A list of the installed Android SDKs. + */ + installedTargets: string[]; + } + + /** + * Object with a single property - projectDir. This is the root directory where NativeScript project is located. + */ + interface IProjectDir { + projectDir: string; + } +} diff --git a/packages/doctor/typings/nativescript-doctor.d.ts b/packages/doctor/typings/nativescript-doctor.d.ts new file mode 100644 index 0000000000..3423e88e14 --- /dev/null +++ b/packages/doctor/typings/nativescript-doctor.d.ts @@ -0,0 +1,10 @@ +/// + + +declare module "@nativescript/doctor" { + export const doctor: NativeScriptDoctor.IDoctor; + export const sysInfo: NativeScriptDoctor.ISysInfo; + export const constants: NativeScriptDoctor.IConstants; + export const androidToolsInfo: NativeScriptDoctor.IAndroidToolsInfo; + export const setShouldCacheSysInfo: (shouldCache: boolean) => void; +}