├── .eslintignore ├── .npmrc ├── .prettierrc.json ├── .editorconfig ├── src ├── core.ts ├── input.ts ├── commands │ ├── cross.ts │ ├── cargo.ts │ └── rustup.ts └── checks.ts ├── jest.config.json ├── .github └── workflows │ └── ci.yml ├── .eslintrc.json ├── tsconfig.json ├── LICENSE ├── package.json └── .gitignore /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @actions-rs:registry=https://npm.pkg.github.com 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | max_line_length = 80 8 | indent_size = 4 9 | 10 | [*.yml] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | export * from './commands/cargo'; 2 | export * from './commands/cross'; 3 | export * from './commands/rustup'; 4 | 5 | import * as input from './input'; 6 | import * as checks from './checks'; 7 | // Re-exports 8 | export { input, checks }; 9 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "clearMocks": true, 3 | "moduleFileExtensions": ["js", "ts"], 4 | "testEnvironment": "node", 5 | "testMatch": ["**/*.test.ts"], 6 | "testRunner": "jest-circus/runner", 7 | "transform": { 8 | "^.+\\.ts$": "ts-jest" 9 | }, 10 | "verbose": true 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [pull_request, push] 2 | jobs: 3 | # The main CI workflow; installs dependencies, runs eslint, prettier, and jest. 4 | main: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - run: npm ci 9 | - run: npm run lint 10 | - run: npm run build 11 | - run: npm run test 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:prettier/recommended", 13 | "prettier", 14 | "prettier/@typescript-eslint" 15 | ], 16 | "rules": { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "checkJs": false, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "newLine": "LF", 10 | "noEmitOnError": true, 11 | "noErrorTruncation": true, 12 | "noFallthroughCasesInSwitch": true, 13 | // TODO: enabling it breaks the `@actions/github` package somehow 14 | "noImplicitAny": false, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "outDir": "dist", 20 | "pretty": true, 21 | "removeComments": true, 22 | "resolveJsonModule": true, 23 | "rootDir": "src", 24 | "strict": true, 25 | "suppressImplicitAnyIndexErrors": false, 26 | "target": "es2018", 27 | "declaration": true, 28 | "sourceMap": true 29 | }, 30 | "include": [ 31 | "src" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 actions-rs team and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/input.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | 3 | /** 4 | * Workaround for a GitHub weird input naming. 5 | * 6 | * For input `all-features: true` it will generate the `INPUT_ALL-FEATURES: true` 7 | * env variable, which looks too weird. 8 | * Here we are trying to get proper name `INPUT_NO_DEFAULT_FEATURES` first, 9 | * and if it does not exist, trying the `INPUT_NO-DEFAULT-FEATURES`. 10 | **/ 11 | export function getInput(name: string, options?: core.InputOptions): string { 12 | const inputFullName = name.replace(/-/g, '_'); 13 | const value = core.getInput(inputFullName, options); 14 | if (value.length > 0) { 15 | return value; 16 | } 17 | 18 | return core.getInput(name, options); 19 | } 20 | 21 | export function getInputBool( 22 | name: string, 23 | options?: core.InputOptions, 24 | ): boolean { 25 | const value = getInput(name, options); 26 | if (value && (value === 'true' || value === '1')) { 27 | return true; 28 | } else { 29 | return false; 30 | } 31 | } 32 | 33 | export function getInputList( 34 | name: string, 35 | options?: core.InputOptions, 36 | ): string[] { 37 | const raw = getInput(name, options); 38 | 39 | return raw 40 | .split(',') 41 | .map((item: string) => item.trim()) 42 | .filter((item: string) => item.length > 0); 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "name": "@actions-rs/core", 4 | "version": "0.0.7", 5 | "author": "actions-rs", 6 | "license": "MIT", 7 | "description": "Core functionality for the @actions-rs repos", 8 | "main": "dist/core.js", 9 | "files": [ 10 | "dist/**/*.js" 11 | ], 12 | "scripts": { 13 | "build": "tsc -p .", 14 | "format": "prettier --write 'src/**/*.{js,ts,tsx}'", 15 | "lint": "tsc --noEmit && eslint 'src/**/*.{js,ts,tsx}'", 16 | "refresh": "rm -rf ./dist/* && npm run build", 17 | "test": "jest --passWithNoTests", 18 | "watch": "tsc -p . -w" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/actions-rs/core.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/actions-rs/core/issues" 26 | }, 27 | "dependencies": { 28 | "@actions/core": "^1.1.3", 29 | "@actions/exec": "^1.0.1", 30 | "@actions/github": "^1.1.0", 31 | "@actions/io": "^1.0.1", 32 | "@actions/tool-cache": "^1.0.1", 33 | "@octokit/graphql": "^2.0.1", 34 | "@octokit/rest": "^16.35.0", 35 | "semver": "^6.3.0" 36 | }, 37 | "devDependencies": { 38 | "@types/jest": "^24.0.23", 39 | "@types/node": "^12.12.5", 40 | "@typescript-eslint/eslint-plugin": "^2.7.0", 41 | "@typescript-eslint/parser": "^2.7.0", 42 | "eslint": "^6.6.0", 43 | "eslint-config-prettier": "^6.6.0", 44 | "eslint-plugin-prettier": "^3.1.1", 45 | "jest": "^24.9.0", 46 | "jest-circus": "^24.9.0", 47 | "prettier": "^1.18.2", 48 | "ts-node": "^8.5.2", 49 | "typescript": "^3.7.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | __tests__/runner/* 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # next.js build output 77 | .next 78 | 79 | # nuxt.js build output 80 | .nuxt 81 | 82 | # vuepress build output 83 | .vuepress/dist 84 | 85 | # Serverless directories 86 | .serverless/ 87 | 88 | # FuseBox cache 89 | .fusebox/ 90 | 91 | # DynamoDB Local files 92 | .dynamodb/ 93 | -------------------------------------------------------------------------------- /src/commands/cross.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import * as io from '@actions/io'; 3 | import * as core from '@actions/core'; 4 | import * as exec from '@actions/exec'; 5 | 6 | import { Cargo } from './cargo'; 7 | 8 | export class Cross { 9 | private readonly path: string; 10 | 11 | private constructor(path: string) { 12 | this.path = path; 13 | } 14 | 15 | public static async getOrInstall(): Promise { 16 | try { 17 | return await Cross.get(); 18 | } catch (error) { 19 | core.debug(`${error}`); 20 | return await Cross.install(); 21 | } 22 | } 23 | 24 | public static async get(): Promise { 25 | const path = await io.which('cross', true); 26 | 27 | return new Cross(path); 28 | } 29 | 30 | public static async install(version?: string): Promise { 31 | const cargo = await Cargo.get(); 32 | 33 | // Somewhat new Rust is required to compile `cross` 34 | // (TODO: Not sure what version exactly, should clarify) 35 | // but if some action will set an override toolchain before this action called 36 | // (ex. `@actions-rs/toolchain` with `toolchain: 1.31.0`) 37 | // `cross` compilation will fail. 38 | // 39 | // In order to skip this problem and install `cross` globally 40 | // using the pre-installed system Rust, 41 | // we are going to jump to the tmpdir (skipping directory override that way) 42 | // install `cross` from there and then jump back. 43 | 44 | const cwd = process.cwd(); 45 | process.chdir(os.tmpdir()); 46 | 47 | try { 48 | const crossPath = await cargo.installCached('cross', version); 49 | return new Cross(crossPath); 50 | } finally { 51 | // It is important to chdir back! 52 | process.chdir(cwd); 53 | core.endGroup(); 54 | } 55 | } 56 | 57 | public async call(args: string[], options?: {}): Promise { 58 | return await exec.exec(this.path, args, options); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/cargo.ts: -------------------------------------------------------------------------------- 1 | import * as io from '@actions/io'; 2 | import * as core from '@actions/core'; 3 | import * as exec from '@actions/exec'; 4 | 5 | export class Cargo { 6 | private readonly path: string; 7 | 8 | private constructor(path: string) { 9 | this.path = path; 10 | } 11 | 12 | public static async get(): Promise { 13 | try { 14 | const path = await io.which('cargo', true); 15 | 16 | return new Cargo(path); 17 | } catch (error) { 18 | core.error( 19 | 'cargo is not installed by default for some virtual environments, \ 20 | see https://help.github.com/en/articles/software-in-virtual-environments-for-github-actions', 21 | ); 22 | core.error( 23 | 'To install it, use this action: https://github.com/actions-rs/toolchain', 24 | ); 25 | 26 | throw error; 27 | } 28 | } 29 | 30 | /** 31 | * Executes `cargo install ${program}`. 32 | * 33 | * TODO: We can utilize the `@actions/tool-cache` and cache installed binary. 34 | * As for now it acts just like an stub and simply installs the program 35 | * on each call. 36 | * 37 | * `version` argument could be either actual program version or `"latest"` string, 38 | * which can be provided by user input. 39 | * 40 | * If `version` is `undefined` or `"latest"`, this method could call the Crates.io API, 41 | * fetch the latest version and search for it in cache. 42 | * TODO: Actually implement this. 43 | * 44 | * ## Returns 45 | * 46 | * Path to the installed program. 47 | * As the $PATH should be already tuned properly at this point, 48 | * returned value at the moment is simply equal to the `program` argument. 49 | */ 50 | public async installCached( 51 | program: string, 52 | version?: string, 53 | ): Promise { 54 | const args = ['install']; 55 | if (version && version != 'latest') { 56 | args.push('--version'); 57 | args.push(version); 58 | } 59 | args.push(program); 60 | 61 | try { 62 | core.startGroup(`Installing "${program} = ${version || 'latest'}"`); 63 | await this.call(args); 64 | } finally { 65 | core.endGroup(); 66 | } 67 | 68 | return program; 69 | } 70 | 71 | /** 72 | * Find the cargo sub-command or install it 73 | */ 74 | public async findOrInstall( 75 | program: string, 76 | version?: string, 77 | ): Promise { 78 | try { 79 | return await io.which(program, true); 80 | } catch (error) { 81 | core.info(`${program} is not installed, installing it now`); 82 | } 83 | 84 | return await this.installCached(program, version); 85 | } 86 | 87 | public async call(args: string[], options?: {}): Promise { 88 | return await exec.exec(this.path, args, options); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/checks.ts: -------------------------------------------------------------------------------- 1 | import * as github from '@actions/github'; 2 | 3 | interface Output { 4 | title: string; 5 | summary: string; 6 | text: string; 7 | } 8 | 9 | /** 10 | * Thin wrapper around the GitHub Checks API 11 | */ 12 | export class CheckReporter { 13 | private readonly client: github.GitHub; 14 | private readonly checkName: string; 15 | private checkId: undefined | number; 16 | 17 | constructor(client: github.GitHub, checkName: string) { 18 | this.client = client; 19 | this.checkName = checkName; 20 | this.checkId = undefined; 21 | } 22 | 23 | /** 24 | * Starts a new Check and returns check ID. 25 | */ 26 | public async startCheck( 27 | status?: 'queued' | 'in_progress' | 'completed', 28 | ): Promise { 29 | const { owner, repo } = github.context.repo; 30 | 31 | const response = await this.client.checks.create({ 32 | owner: owner, 33 | repo: repo, 34 | name: this.checkName, 35 | head_sha: github.context.sha, // eslint-disable-line 36 | status: status ? status : 'in_progress', 37 | }); 38 | // TODO: Check for errors 39 | 40 | this.checkId = response.data.id; 41 | return this.checkId; 42 | } 43 | 44 | // TODO: 45 | // public async sendAnnotations(annotations: Array): Promise { 46 | // } 47 | 48 | /** 49 | * It is up to caller to call the `startCheck` first! 50 | */ 51 | public async finishCheck( 52 | conclusion: 53 | | 'cancelled' 54 | | 'success' 55 | | 'failure' 56 | | 'neutral' 57 | | 'timed_out' 58 | | 'action_required', 59 | output: Output, 60 | ): Promise { 61 | const { owner, repo } = github.context.repo; 62 | 63 | // TODO: Check for errors 64 | await this.client.checks.update({ 65 | owner: owner, 66 | repo: repo, 67 | name: this.checkName, 68 | check_run_id: this.checkId!, // eslint-disable-line 69 | status: 'completed', 70 | conclusion: conclusion, 71 | completed_at: new Date().toISOString(), // eslint-disable-line 72 | output: output, 73 | }); 74 | 75 | return; 76 | } 77 | 78 | public async cancelCheck(): Promise { 79 | const { owner, repo } = github.context.repo; 80 | 81 | // TODO: Check for errors 82 | await this.client.checks.update({ 83 | owner: owner, 84 | repo: repo, 85 | name: this.checkName, 86 | check_run_id: this.checkId!, // eslint-disable-line 87 | status: 'completed', 88 | conclusion: 'cancelled', 89 | completed_at: new Date().toISOString(), // eslint-disable-line 90 | output: { 91 | title: this.checkName, 92 | summary: 'Unhandled error', 93 | text: 94 | 'Check was cancelled due to unhandled error. Check the Action logs for details.', 95 | }, 96 | }); 97 | 98 | return; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/commands/rustup.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import * as path from 'path'; 3 | import * as process from 'process'; 4 | 5 | import * as semver from 'semver'; 6 | import * as io from '@actions/io'; 7 | import * as core from '@actions/core'; 8 | import * as exec from '@actions/exec'; 9 | import * as tc from '@actions/tool-cache'; 10 | 11 | const PROFILES_MIN_VERSION = '1.20.1'; 12 | const COMPONENTS_MIN_VERSION = '1.20.1'; 13 | 14 | type Profile = 'minimal' | 'default' | 'full'; 15 | 16 | export interface ToolchainOptions { 17 | default?: boolean; 18 | override?: boolean; 19 | components?: string[]; 20 | noSelfUpdate?: boolean; 21 | } 22 | 23 | export class RustUp { 24 | private readonly path: string; 25 | 26 | private constructor(exePath: string) { 27 | this.path = exePath; 28 | } 29 | 30 | public static async getOrInstall(): Promise { 31 | try { 32 | return await RustUp.get(); 33 | } catch (error) { 34 | core.debug( 35 | `Unable to find "rustup" executable, installing it now. Reason: ${error}`, 36 | ); 37 | return await RustUp.install(); 38 | } 39 | } 40 | 41 | // Will throw an error if `rustup` is not installed. 42 | public static async get(): Promise { 43 | const exePath = await io.which('rustup', true); 44 | 45 | return new RustUp(exePath); 46 | } 47 | 48 | public static async install(): Promise { 49 | const args = [ 50 | '--default-toolchain', 51 | 'none', 52 | '-y', // No need for the prompts (hard error from within the Docker containers) 53 | ]; 54 | 55 | switch (process.platform) { 56 | case 'darwin': 57 | case 'linux': { 58 | // eslint-disable-line prettier/prettier 59 | const rustupSh = await tc.downloadTool('https://sh.rustup.rs'); 60 | 61 | // While the `rustup-init.sh` is properly executed as is, 62 | // when Action is running on the VM itself, 63 | // it fails with `EACCES` when called in the Docker container. 64 | // Adding the execution bit manually just in case. 65 | // See: https://github.com/actions-rs/toolchain/pull/19#issuecomment-543358693 66 | core.debug(`Executing chmod 755 on the ${rustupSh}`); 67 | await fs.chmod(rustupSh, 0o755); 68 | 69 | await exec.exec(rustupSh, args); 70 | break; 71 | } 72 | 73 | case 'win32': { 74 | const rustupExe = await tc.downloadTool('http://win.rustup.rs'); 75 | await exec.exec(rustupExe, args); 76 | break; 77 | } 78 | 79 | default: 80 | throw new Error( 81 | `Unknown platform ${process.platform}, can't install rustup`, 82 | ); 83 | } 84 | 85 | // `$HOME` should always be declared, so it is more to get the linters happy 86 | core.addPath(path.join(process.env.HOME!, '.cargo', 'bin')); // eslint-disable-line @typescript-eslint/no-non-null-assertion 87 | 88 | // Assuming it is in the $PATH already 89 | return new RustUp('rustup'); 90 | } 91 | 92 | public async installToolchain( 93 | name: string, 94 | options?: ToolchainOptions, 95 | ): Promise { 96 | const args = ['toolchain', 'install', name]; 97 | if (options && options.components && options.components.length > 0) { 98 | for (const component of options.components) { 99 | args.push('--component'); 100 | args.push(component); 101 | } 102 | } 103 | if (options && options.noSelfUpdate) { 104 | args.push('--no-self-update'); 105 | } 106 | await this.call(args); 107 | 108 | if (options && options.default) { 109 | await this.call(['default', name]); 110 | } 111 | 112 | if (options && options.override) { 113 | await this.call(['override', 'set', name]); 114 | } 115 | 116 | // TODO: Is there smth like Rust' `return Ok(())`? 117 | return 0; 118 | } 119 | 120 | public async addTarget( 121 | name: string, 122 | forToolchain?: string, 123 | ): Promise { 124 | const args = ['target', 'add']; 125 | if (forToolchain) { 126 | args.push('--toolchain'); 127 | args.push(forToolchain); 128 | } 129 | args.push(name); 130 | 131 | return await this.call(args); 132 | } 133 | 134 | public async activeToolchain(): Promise { 135 | const stdout = await this.callStdout(['show', 'active-toolchain']); 136 | 137 | if (stdout) { 138 | return stdout.split(' ', 2)[0]; 139 | } else { 140 | throw new Error('Unable to determine active toolchain'); 141 | } 142 | } 143 | 144 | public async supportProfiles(): Promise { 145 | const version = await this.version(); 146 | const supports = semver.gte(version, PROFILES_MIN_VERSION); 147 | if (supports) { 148 | core.info(`Installed rustup ${version} support profiles`); 149 | } else { 150 | core.info(`Installed rustup ${version} does not support profiles, \ 151 | expected at least ${PROFILES_MIN_VERSION}`); 152 | } 153 | return supports; 154 | } 155 | 156 | public async supportComponents(): Promise { 157 | const version = await this.version(); 158 | const supports = semver.gte(version, COMPONENTS_MIN_VERSION); 159 | if (supports) { 160 | core.info(`Installed rustup ${version} support components`); 161 | } else { 162 | core.info(`Installed rustup ${version} does not support components, \ 163 | expected at least ${PROFILES_MIN_VERSION}`); 164 | } 165 | return supports; 166 | } 167 | 168 | /** 169 | * Executes `rustup set profile ${name}` 170 | * 171 | * Note that it includes the check if currently installed rustup support profiles at all 172 | */ 173 | public async setProfile(name: Profile): Promise { 174 | return await this.call(['set', 'profile', name]); 175 | } 176 | 177 | public async version(): Promise { 178 | const stdout = await this.callStdout(['-V']); 179 | 180 | return stdout.split(' ')[1]; 181 | } 182 | 183 | // rustup which `program` 184 | public async which(program: string): Promise { 185 | const stdout = await this.callStdout(['which', program]); 186 | 187 | if (stdout) { 188 | return stdout; 189 | } else { 190 | throw new Error(`Unable to find the ${program}`); 191 | } 192 | } 193 | 194 | public async selfUpdate(): Promise { 195 | return await this.call(['self', 'update']); 196 | } 197 | 198 | public async call(args: string[], options?: {}): Promise { 199 | return await exec.exec(this.path, args, options); 200 | } 201 | 202 | /** 203 | * Call the `rustup` and return an stdout 204 | */ 205 | async callStdout(args: string[], options?: {}): Promise { 206 | let stdout = ''; 207 | const resOptions = Object.assign({}, options, { 208 | listeners: { 209 | stdout: (buffer: Buffer) => { 210 | stdout += buffer.toString(); 211 | }, 212 | }, 213 | }); 214 | 215 | await this.call(args, resOptions); 216 | 217 | return stdout; 218 | } 219 | } 220 | --------------------------------------------------------------------------------