From 3e49514346935ad552f311edd4b42bef301de5f9 Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuk Date: Thu, 25 May 2017 17:20:31 +0300 Subject: [PATCH] Implement basic features (#1) Features include: - Automatic PlatformIO Core installation - Project Initialization - C/C++ Autocomplete Index Rebuild --- .babelrc | 13 + .esformatter | 7 + .eslintignore | 3 + .eslintrc.js | 51 +++ .gitignore | 3 + .vscodeignore | 9 + CHANGELOG.md | 8 + package.json | 79 +++++ src/commands/init.js | 74 ++++ src/constants.js | 35 ++ src/extension.js | 99 ++++++ src/index.js | 12 + src/installer/helpers.js | 108 ++++++ src/installer/manager.js | 93 +++++ src/installer/python-install-confirm.js | 36 ++ src/installer/stages/base.js | 59 ++++ src/installer/stages/platformio-core.js | 350 +++++++++++++++++++ src/installer/vscode-global-state-storage.js | 40 +++ src/project/indexer.js | 221 ++++++++++++ src/utils.js | 135 +++++++ 20 files changed, 1435 insertions(+) create mode 100644 .babelrc create mode 100644 .esformatter create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .vscodeignore create mode 100644 CHANGELOG.md create mode 100644 package.json create mode 100644 src/commands/init.js create mode 100644 src/constants.js create mode 100644 src/extension.js create mode 100644 src/index.js create mode 100644 src/installer/helpers.js create mode 100644 src/installer/manager.js create mode 100644 src/installer/python-install-confirm.js create mode 100644 src/installer/stages/base.js create mode 100644 src/installer/stages/platformio-core.js create mode 100644 src/installer/vscode-global-state-storage.js create mode 100644 src/project/indexer.js create mode 100644 src/utils.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..cc2da71 --- /dev/null +++ b/.babelrc @@ -0,0 +1,13 @@ +{ + "plugins": [ + "transform-class-properties" + ], + "presets": [ + ["env", { + "targets": { + "node": "7.4" + } + }] + ], + "sourceMap": "inline" +} diff --git a/.esformatter b/.esformatter new file mode 100644 index 0000000..1cfa21e --- /dev/null +++ b/.esformatter @@ -0,0 +1,7 @@ +{ + "root": true, + + "indent": { + "value": " " + } +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..24fa511 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +!.eslintrc.js +/node_modules/** +/lib/** diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..4ee43d8 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,51 @@ +module.exports = { + 'env': { + 'browser': false, + 'es6': true, + 'node': true, + 'jasmine': true, + }, + 'extends': ['eslint:recommended'], + 'parser': 'babel-eslint', + 'parserOptions': { + 'ecmaVersion': 6, + 'sourceType': 'module', + }, + 'plugins': [ + 'sort-imports-es6-autofix', + ], + 'rules': { + 'brace-style': ['error', '1tbs'], + 'comma-dangle': ['error', { + 'arrays': 'always-multiline', + 'objects': 'always-multiline', + 'functions': 'never', + }], + 'curly': ['warn', 'all'], + 'indent': [ + 'warn', + 2, + { + 'SwitchCase': 1, + }, + ], + 'linebreak-style': ['error', 'unix'], + 'no-console': [ + 'error', + { + 'allow': ['log', 'error'], + }, + ], + 'no-var': 'error', + 'object-curly-spacing': ['warn', 'always'], + 'prefer-const': 'error', + 'quotes': ['error', 'single', 'avoid-escape'], + 'semi': ['error', 'always'], + 'sort-imports-es6-autofix/sort-imports-es6': [2, { + 'ignoreCase': false, + 'ignoreMemberSort': false, + 'memberSyntaxSortOrder': ['none', 'all', 'multiple', 'single'], + }], + 'space-infix-ops': 'warn', + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb4bc95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/lib/ +*.vsix diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..ef13632 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +.vscode-test/** +src/** +test/** +.babelrc +.esformatter +.eslintignore +.eslintrc.js +.gitignore diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..81862ce --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Change Log + +All notable changes to the "platformio-ide" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] +- Initial release diff --git a/package.json b/package.json new file mode 100644 index 0000000..84f004b --- /dev/null +++ b/package.json @@ -0,0 +1,79 @@ +{ + "name": "platformio-ide", + "displayName": "PlatformIO IDE", + "description": "Official PlatformIO IDE for Visual Studio Code. PlatformIO is an open source ecosystem for IoT development. Cross-platform build system and library manager. Contin", + "version": "0.0.1-beta.0", + "publisher": "platformio", + "license": "Apache-2.0", + "engines": { + "vscode": "^1.12.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "main": "./lib/index", + "contributes": { + "commands": [ + { + "command": "platformio-ide.init-project", + "title": "PlatformIO: Initialize project" + }, + { + "command": "platformio-ide.rebuild-index", + "title": "PlatformIO: Rebuild index" + } + ], + "configuration": { + "type": "object", + "title": "PlatformIO IDE configuration", + "properties": { + "platformio-ide.useBuiltinPIOCore": { + "type": "boolean", + "default": true, + "description": "Use built-in PlatformIO Core" + }, + "platformio-ide.useDevelopmentPIOCore": { + "type": "boolean", + "default": false, + "description": "Use development version of PlatformIO Core" + }, + "platformio-ide.autoRebuildAutocompleteIndex": { + "type": "boolean", + "default": true, + "description": "Automatically rebuild C/C++ Project Index when platformio.ini is changed or when new libraries are installed." + } + } + } + }, + "scripts": { + "lint": "eslint .eslintrc.js src", + "format": "esformatter -i .eslintrc.js 'src/**.js' && eslint --fix .eslintrc.js src", + "build": "babel src --out-dir lib && echo 'Done!'", + "postinstall": "node ./node_modules/vscode/bin/install", + "test": "node ./node_modules/vscode/bin/test", + "vscode:prepublish": "npm run build" + }, + "devDependencies": { + "@types/node": "^7.0.21", + "babel-cli": "^6.24.1", + "babel-eslint": "^7.2.3", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-preset-env": "^1.5.1", + "esformatter": "^0.10.0", + "eslint": "^3.19.0", + "eslint-plugin-sort-imports-es6-autofix": "^0.1.1", + "vscode": "^1.0.0" + }, + "dependencies": { + "cross-spawn": "^5.1.0", + "fs-plus": "^3.0.0", + "open": "^0.0.5", + "request": "^2.81.0", + "semver": "^5.3.0", + "tar": "^2.2.1", + "tmp": "^0.0.31" + } +} diff --git a/src/commands/init.js b/src/commands/init.js new file mode 100644 index 0000000..981c345 --- /dev/null +++ b/src/commands/init.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import { runPioCommand } from '../utils'; +import vscode from 'vscode'; + +export default async function initCommand() { + if (!vscode.workspace.rootPath) { + vscode.window.showWarningMessage( + 'PlatformIO projec could not be initialized. Please open a folder ' + + 'first before performing initialization.' + ); + } + await vscode.window.withProgress({ + title: 'PlatformIO Project initialization', + location: vscode.ProgressLocation.Window, + }, async (progress) => { + progress.report({ + message: 'Updating a list of avaialbe boards', + }); + + try { + const data = JSON.parse(await new Promise((resolve, reject) => { + runPioCommand(['boards', '--json-output'], (code, stdout, stderr) => { + if (code !== 0) { + reject(stderr); + } else { + resolve(stdout); + } + }); + })); + const items = data.map((board) => ({ + label: board.name, + description: board.vendor, + detail: board.mcu, + boardId: board.id, + })); + + progress.report({ + message: 'Selecting a board', + }); + const selectedBoard = await vscode.window.showQuickPick(items, { + ignoreFocusOut: true, + matchOnDescription: true, + matchOnDetail: true, + placeHolder: 'Select a board', + }); + + if (selectedBoard) { + progress.report({ + message: 'Performing initialization', + }); + + await new Promise((resolve, reject) => { + runPioCommand(['init', '--board', selectedBoard.boardId, '--project-dir', vscode.workspace.rootPath], (code, stdout, stderr) => { + if (code !== 0) { + reject(stderr); + } else { + resolve(stdout); + } + }); + }); + } + } catch (error) { + console.error(error); + vscode.window.showErrorMessage(error); + } + }); +} diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..87dd32c --- /dev/null +++ b/src/constants.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import fs from 'fs-plus'; +import path from 'path'; + +export const DEFAULT_PIO_ARGS = ['-f']; +export const AUTO_REBUILD_DELAY = 3000; +export const IS_WINDOWS = process.platform.startsWith('win'); +export const PIO_HOME_DIR = _getPioHomeDir(process.env.PLATFORMIO_HOME_DIR || path.join(fs.getHomeDirectory(), '.platformio')); +export const ENV_DIR = path.join(PIO_HOME_DIR, 'penv'); +export const ENV_BIN_DIR = path.join(ENV_DIR, IS_WINDOWS ? 'Scripts' : 'bin'); +export const PIO_CORE_MIN_VERSION = '3.4.0-b.1'; + +function _getPioHomeDir(pioHomeDir) { + if (IS_WINDOWS) { + // Make sure that all path characters have valid ASCII codes. + for (const char of pioHomeDir) { + if (char.charCodeAt(0) > 127) { + // If they don't, put the pio home directory into the root of the disk. + const homeDirPathFormat = path.parse(pioHomeDir); + return path.format({ + dir: homeDirPathFormat.root, + base: '.platformio', + }); + } + } + } + return pioHomeDir; +} diff --git a/src/extension.js b/src/extension.js new file mode 100644 index 0000000..c892079 --- /dev/null +++ b/src/extension.js @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import { ensureDirExists } from './utils'; + +import InstallationManager from './installer/manager'; +import ProjectIndexer from './project/indexer'; +import initCommand from './commands/init'; +import path from 'path'; +import semver from 'semver'; +import vscode from 'vscode'; + +export default class PlatformIOVSCodeExtension { + + constructor() { + this.activate = this.activate.bind(this); + + const min = 100; + const max = 999; + this.instanceId = Math.floor(Math.random() * (max - min)) + min; + } + + async activate(context) { + if (!vscode.workspace.rootPath) { + return; + } + + const ext = vscode.extensions.getExtension('platformio.platformio-ide'); + const isPrerelease = Boolean(semver.prerelease(ext.packageJSON.version)); + + await this.startInstaller(context.globalState, context.extensionPath, isPrerelease); + + const indexer = new ProjectIndexer(vscode.workspace.rootPath); + context.subscriptions.push(indexer); + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => indexer.toggle())); + + await indexer.toggle(); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'platformio-ide.init-project', + initCommand) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'platformio-ide.rebuild-index', + () => indexer.doRebuild({ + verbose: true, + })) + ); + } + + startInstaller(globalState, extensionPath, isPrerelease) { + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: 'PlatformIO', + }, async (progress) => { + progress.report({ + message: 'Verifying PlatformIO Core installation...', + }); + + const cacheDir = path.join(extensionPath, '.cache'); + await ensureDirExists(cacheDir); + + const config = vscode.workspace.getConfiguration('platformio-ide'); + const im = new InstallationManager(globalState, config, cacheDir, isPrerelease); + + if (im.locked()) { + vscode.window.showInformationMessage( + 'PlatformIO IDE installation has been suspended, because PlatformIO ' + + 'IDE Installer is already started in another window.'); + } else if (await im.check()) { + return; + } else { + progress.report({ + message: 'Installing PlatformIO IDE...', + }); + try { + im.lock(); + await im.install(); + } catch (err) { + vscode.window.showErrorMessage(err.toString(), { + modal: true, + }); + } finally { + im.unlock(); + } + } + im.destroy(); + return Promise.reject(null); + }); + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..1f7e82a --- /dev/null +++ b/src/index.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import PlatformIOVSCodeExtension from './extension'; + +const extension = new PlatformIOVSCodeExtension(); +module.exports = extension; diff --git a/src/installer/helpers.js b/src/installer/helpers.js new file mode 100644 index 0000000..dd210ba --- /dev/null +++ b/src/installer/helpers.js @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import fs from 'fs-plus'; +import path from 'path'; +import request from 'request'; +import tar from 'tar'; +import zlib from 'zlib'; + + +export function PEPverToSemver(pepver) { + return pepver.replace(/(\.\d+)\.?(dev|a|b|rc|post)/, '$1-$2.'); +} + +export async function download(source, target, retries = 3) { + const contentLength = await getContentLength(source); + + if (fileExistsAndSizeMatches(target, contentLength)) { + return target; + } + + let lastError = null; + + while (retries >= 0) { + try { + await _download(source, target); + if (fileExistsAndSizeMatches(target, contentLength)) { + return target; + } + } catch (error) { + lastError = error; + console.error(error); + } + + retries--; + } + + if (lastError) { + throw lastError; + } else { + throw new Error(`Failed to download file ${path.basename(target)}`); + } +} + + +function fileExistsAndSizeMatches(target, contentLength) { + if (fs.isFileSync(target)) { + if (contentLength > 0 && contentLength == fs.getSizeSync(target)) { + return true; + } + try { + fs.removeSync(target); + } catch (err) { + console.error(err); + } + } + return false; +} + +async function _download(source, target) { + return new Promise((resolve, reject) => { + const proxy = (process.env.HTTPS_PROXY && process.env.HTTPS_PROXY.trim() + || process.env.HTTP_PROXY && process.env.HTTP_PROXY.trim()); + const file = fs.createWriteStream(target); + const options = { + url: source, + }; + if (proxy) { + options.proxy = proxy; + } + request.get(options) + .on('error', err => reject(err)) + .pipe(file); + file.on('error', err => reject(err)); + file.on('finish', () => resolve(target)); + }); +} + +function getContentLength(url) { + return new Promise(resolve => { + request.head({ + url, + }, (err, response) => { + if (err || response.statusCode !== 200 || !response.headers.hasOwnProperty('content-length')) { + resolve(-1); + } + resolve(parseInt(response.headers['content-length'])); + }); + }); +} + +export function extractTarGz(source, destination) { + return new Promise((resolve, reject) => { + fs.createReadStream(source) + .pipe(zlib.createGunzip()) + .on('error', err => reject(err)) + .pipe(tar.Extract({ + path: destination, + })) + .on('error', err => reject(err)) + .on('end', () => resolve(destination)); + }); +} diff --git a/src/installer/manager.js b/src/installer/manager.js new file mode 100644 index 0000000..1cc7b39 --- /dev/null +++ b/src/installer/manager.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import PlatformIOCoreStage from './stages/platformio-core'; +import VscodeGlobalStateStorage from './vscode-global-state-storage'; +import VscodePythonInstallConfirm from './python-install-confirm'; +import vscode from 'vscode'; + + +export default class InstallationManager { + + LOCK_TIMEOUT =1 * 60 * 1000; // 1 minute + LOCK_KEY = 'platformio-ide:installer-lock'; + STORAGE_STATE_KEY = 'platformio-ide:installer-state'; + + constructor(globalState, config, cacheDir, isPrerelease) { + this.globalState = globalState; + this.stateStorage = new VscodeGlobalStateStorage(globalState, this.STORAGE_STATE_KEY); + + this.stages = [ + new PlatformIOCoreStage(this.onDidStatusChange.bind(this), this.stateStorage, { + useBuiltinPIOCore: config.get('useBuiltinPIOCore'), + setUseBuiltinPIOCore: (value) => config.update('platformio-ide.useBuiltinPIOCore', value), + useDevelopmentPIOCore: config.get('useDevelopmentPIOCore') || true, // FIXME: remove "|| true" when released + installConfirm: new VscodePythonInstallConfirm(), + cacheDir: cacheDir, + isPrerelease: isPrerelease, + }), + ]; + } + + onDidStatusChange() { + // increase lock timeout on each stage update + if (this.locked()) { + this.lock(); + } + } + + lock() { + return this.globalState.update(this.LOCK_KEY, new Date().getTime()); + } + + unlock() { + return this.globalState.update(this.LOCK_KEY, undefined); + } + + locked() { + const lockTime = this.globalState.get(this.LOCK_KEY); + if (!lockTime) { + return false; + } + return (new Date().getTime() - parseInt(lockTime)) <= this.LOCK_TIMEOUT; + } + + async check() { + let result = true; + for (const stage of this.stages) { + try { + if (!(await stage.check())) { + result = false; + } + } catch (err) { + result = false; + console.log(err); + } + } + return result; + } + + async install() { + await Promise.all(this.stages.map(stage => stage.install())); + + const result = await vscode.window.showInformationMessage( + 'PlatformIO IDE has been successfully installed! Please reload window', + 'Reload Now' + ); + + if (result === 'Reload Now') { + vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + } + + + destroy() { + return this.stages.map(stage => stage.destroy()); + } + +} diff --git a/src/installer/python-install-confirm.js b/src/installer/python-install-confirm.js new file mode 100644 index 0000000..ad16ac0 --- /dev/null +++ b/src/installer/python-install-confirm.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import open from 'open'; +import vscode from 'vscode'; + +export default class VscodePythonInstallConfirm { + + TRY_AGAIN = 0; + ABORT = 1; + + async requestPythonInstall() { + const selectedItem = await vscode.window.showInformationMessage( + 'PlatformIO: Can not find Python 2.7 Interpreter', + { title: 'Install Python 2.7', isCloseAffordance: true }, + { title: 'I have Python 2.7', isCloseAffordance: true }, + { title: 'Try again', isCloseAffordance: true }, + { title: 'Abort PlatformIO IDE Installation', isCloseAffordance: true } + ); + + switch (selectedItem.title) { + case 'Install Python 2.7': + open('http://docs.platformio.org/page/faq.html#install-python-interpreter'); + return this.TRY_AGAIN; + case 'Abort PlatformIO IDE Installation': + return this.ABORT; + default: + return this.TRY_AGAIN; + } + } +} diff --git a/src/installer/stages/base.js b/src/installer/stages/base.js new file mode 100644 index 0000000..307a7ed --- /dev/null +++ b/src/installer/stages/base.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +export default class BaseStage { + + static STATUS_CHECKING = 0; + static STATUS_INSTALLING = 1; + static STATUS_SUCCESSED = 2; + static STATUS_FAILED = 3; + + constructor(onStatusChange, stateStorage, params = {}) { + this._onStatusChange = onStatusChange; + this._stateStorage = stateStorage; + this.params = params; + + this._status = BaseStage.STATUS_CHECKING; + } + + get name() { + return 'Stage'; + } + + get status() { + return this._status; + } + + set status(status) { + this._status = status; + this._onStatusChange(); + } + + get stateKey() { + return this.constructor.name; + } + + get state() { + return this._stateStorage.getValue(this.stateKey); + } + + set state(value) { + this._stateStorage.setValue(this.stateKey, value); + } + + check() { + throw new Error('Stage must implement a `check` method'); + } + + install() { + throw new Error('Stage must implement an `install` method'); + } + + destroy() {} + +} diff --git a/src/installer/stages/platformio-core.js b/src/installer/stages/platformio-core.js new file mode 100644 index 0000000..72f89a7 --- /dev/null +++ b/src/installer/stages/platformio-core.js @@ -0,0 +1,350 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import * as utils from '../../utils'; + +import { ENV_BIN_DIR, ENV_DIR, IS_WINDOWS, PIO_CORE_MIN_VERSION, PIO_HOME_DIR } from '../../constants'; +import { PEPverToSemver, download, extractTarGz } from '../helpers'; + +import BaseStage from './base'; +import fs from 'fs-plus'; +import path from 'path'; +import semver from 'semver'; +import tmp from 'tmp'; + +export default class PlatformIOCoreStage extends BaseStage { + + static UPGRADE_PIOCORE_TIMEOUT = 86400 * 3 * 1000; // 3 days + + static pythonVersion = '2.7.13'; + static vitrualenvUrl = 'https://pypi.python.org/packages/source/v/virtualenv/virtualenv-14.0.6.tar.gz'; + + constructor() { + super(...arguments); + tmp.setGracefulCleanup(); + } + + get name() { + return 'PlatformIO Core'; + } + + async check() { + if (this.params.useBuiltinPIOCore) { + if (!fs.isDirectorySync(ENV_BIN_DIR)) { + throw new Error('Virtual environment is not created'); + } + try { + await this.autoUpgradePIOCore(); + } catch (err) { + console.log(err); + } + } + + const customDirs = this.params.useBuiltinPIOCore ? [ENV_BIN_DIR] : null; + const coreVersion = await utils.getCoreVersion(await utils.getPythonExecutable(customDirs)); + if (semver.lt(PEPverToSemver(coreVersion), PIO_CORE_MIN_VERSION)) { + this.params.setUseBuiltinPIOCore(true); + throw new Error(`Incompatible PIO Core ${coreVersion}`); + } + + this.status = BaseStage.STATUS_SUCCESSED; + console.log(`Found PIO Core ${coreVersion}`); + return true; + } + + async install() { + if (this.status === BaseStage.STATUS_SUCCESSED) { + return true; + } + if (!this.params.useBuiltinPIOCore) { + this.status = BaseStage.STATUS_SUCCESSED; + return true; + } + this.status = BaseStage.STATUS_INSTALLING; + + await this.cleanVirtualEnvDir(); + + if (await this.isCondaInstalled()) { + await this.createVirtualenvWithConda(); + } else { + const pythonExecutable = await this.whereIsPython(); + if (!pythonExecutable) { + this.status = BaseStage.STATUS_FAILED; + throw new Error('Can not find Python Interpreter'); + } + try { + await this.createVirtualenvWithUser(pythonExecutable); + } catch (err) { + console.error(err); + await this.createVirtualenvWithDownload(pythonExecutable); + } + } + + const envPythonExecutable = await utils.getPythonExecutable([ENV_BIN_DIR]); + if (!envPythonExecutable) { + throw new Error('Python interpreter not found'); + } + + await this.installPIOCore(envPythonExecutable); + + this.status = BaseStage.STATUS_SUCCESSED; + return true; + } + + async whereIsPython() { + let status = this.params.installConfirm.TRY_AGAIN; + do { + const pythonExecutable = await utils.getPythonExecutable(); + if (pythonExecutable) { + return pythonExecutable; + } + + if (IS_WINDOWS) { + try { + return await this.installPythonForWindows(); + } catch (err) { + console.error(err); + } + } + + status = await this.params.installConfirm.requestPythonInstall(); + + } while (status !== this.params.installConfirm.ABORT); + return null; + } + + async installPythonForWindows() { + // https://www.python.org/ftp/python/2.7.13/python-2.7.13.msi + // https://www.python.org/ftp/python/2.7.13/python-2.7.13.amd64.msi + const pythonArch = process.arch === 'x64' ? '.amd64' : ''; + const msiUrl = `https://www.python.org/ftp/python/${PlatformIOCoreStage.pythonVersion}/python-${PlatformIOCoreStage.pythonVersion}${pythonArch}.msi`; + const msiInstaller = await download( + msiUrl, + path.join(this.params.cacheDir, path.basename(msiUrl)) + ); + const targetDir = path.join(PIO_HOME_DIR, 'python27'); + const pythonPath = path.join(targetDir, 'python.exe'); + + if (!fs.isFileSync(pythonPath)) { + try { + await this.installPythonFromWindowsMSI(msiInstaller, targetDir); + } catch (err) { + console.error(err); + await this.installPythonFromWindowsMSI(msiInstaller, targetDir, true); + } + } + + // append temporary to system environment + process.env.PATH = [targetDir, path.join(targetDir, 'Scripts'), process.env.PATH].join(path.delimiter); + process.env.Path = process.env.PATH; + + // install virtualenv + return new Promise(resolve => { + utils.spawnCommand( + pythonPath, + ['-m', 'pip', 'install', 'virtualenv'], + () => resolve(pythonPath) + ); + }); + } + + async installPythonFromWindowsMSI(msiInstaller, targetDir, administrative = false) { + const logFile = path.join(this.params.cacheDir, 'python27msi.log'); + await new Promise((resolve, reject) => { + utils.spawnCommand( + 'msiexec.exe', + [administrative ? '/a' : '/i', msiInstaller, '/qn', '/li', logFile, `TARGETDIR=${targetDir}`], + async (code, stdout, stderr) => { + if (code === 0) { + return resolve(stdout); + } else { + if (fs.isFileSync(logFile)) { + stderr = fs.readFileSync(logFile).toString(); + } + return reject(`MSI Python2.7: ${stderr}`); + } + }, + { + spawnOptions: { + shell: true, + }, + } + ); + }); + if (!fs.isFileSync(path.join(targetDir, 'python.exe'))) { + throw new Error('Could not install Python 2.7 using MSI'); + } + } + + async cleanVirtualEnvDir() { + if (fs.isFileSync(ENV_DIR)) { + try { + fs.removeSync(ENV_DIR); + } catch (err) { + console.error(err); + } + } + } + + isCondaInstalled() { + return new Promise(resolve => { + utils.spawnCommand('conda', ['--version'], code => resolve(code === 0)); + }); + } + + createVirtualenvWithConda() { + return new Promise((resolve, reject) => { + utils.spawnCommand( + 'conda', + ['create', '--yes', '--quiet', 'python=2', '--prefix', ENV_DIR], + (code, stdout, stderr) => { + if (code === 0) { + return resolve(stdout); + } else { + return reject(`Conda Virtualenv: ${stderr}`); + } + } + ); + }); + } + + createVirtualenvWithUser(pythonExecutable) { + return new Promise((resolve, reject) => { + utils.spawnCommand( + 'virtualenv', + ['-p', pythonExecutable, ENV_DIR], + (code, stdout, stderr) => { + if (code === 0) { + return resolve(stdout); + } else { + return reject(`User's Virtualenv: ${stderr}`); + } + } + ); + }); + } + + createVirtualenvWithDownload(pythonExecutable) { + return new Promise((resolve, reject) => { + download( + PlatformIOCoreStage.vitrualenvUrl, + path.join(this.params.cacheDir, 'virtualenv.tar.gz') + ).then(archivePath => { + const tmpDir = tmp.dirSync({ + unsafeCleanup: true, + }); + extractTarGz(archivePath, tmpDir.name).then(dstDir => { + const virtualenvScript = fs.listTreeSync(dstDir).find( + item => path.basename(item) === 'virtualenv.py'); + if (!virtualenvScript) { + return reject('Can not find virtualenv.py script'); + } + utils.spawnCommand( + pythonExecutable, + [virtualenvScript, ENV_DIR], + (code, stdout, stderr) => { + if (code === 0) { + return resolve(stdout); + } else { + return reject(`Virtualenv Download: ${stderr}`); + } + } + ); + }); + }).catch(err => reject(`Virtualenv Download: ${err}`)); + }); + } + + async installPIOCore(pythonExecutable) { + const cmd = pythonExecutable; + const args = ['-m', 'pip', 'install', '--no-cache-dir', '-U']; + if (this.params.useDevelopmentPIOCore) { + args.push('https://github.com/platformio/platformio/archive/develop.zip'); + } else { + args.push('platformio'); + } + try { + await new Promise((resolve, reject) => { + utils.spawnCommand(cmd, args, (code, stdout, stderr) => { + if (code === 0) { + resolve(stdout); + } else { + reject(`PIP: ${stderr}`); + } + }); + }); + } catch (err) { + console.error(err); + // Old versions of PIP don't support `--no-cache-dir` option + return new Promise((resolve, reject) => { + utils.spawnCommand( + cmd, + args.filter(arg => arg !== '--no-cache-dir'), + (code, stdout, stderr) => { + if (code === 0) { + resolve(stdout); + } else { + reject(`PIP: ${stderr}`); + } + } + ); + }); + } + } + + initState() { + let state = this.state; + if (!state || !state.hasOwnProperty('pioCoreChecked') || !state.hasOwnProperty('lastIDEVersion')) { + state = { + pioCoreChecked: 0, + lastIDEVersion: null, + }; + } + return state; + } + + async autoUpgradePIOCore() { + const newState = this.initState(); + const now = new Date().getTime(); + if ((now - PlatformIOCoreStage.UPGRADE_PIOCORE_TIMEOUT) > parseInt(newState.pioCoreChecked)) { + newState.pioCoreChecked = now; + // PIO Core + await new Promise(resolve => { + utils.runPioCommand( + ['upgrade'], + (code, stdout, stderr) => { + if (code !== 0) { + console.error(stdout, stderr); + } + resolve(true); + }, + { + busyTitle: 'Upgrading PIO Core', + } + ); + }); + // PIO Core Packages + await new Promise(resolve => { + utils.runPioCommand( + ['update', '--core-packages'], + (code, stdout, stderr) => { + if (code !== 0) { + console.error(stdout, stderr); + } + resolve(true); + }, + { + busyTitle: 'Updating PIO Core packages', + } + ); + }); + } + this.state = newState; + } + +} diff --git a/src/installer/vscode-global-state-storage.js b/src/installer/vscode-global-state-storage.js new file mode 100644 index 0000000..0e28471 --- /dev/null +++ b/src/installer/vscode-global-state-storage.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +export default class VscodeGlobalStateStorage { + + constructor(globalState, stateKey) { + this._globalState = globalState; + this._stateKey = stateKey; + } + + _loadState() { + try { + const value = this._globalState.get(this._stateKey); + return value || {}; + } catch (error) { + console.error(error); + return {}; + } + } + + getValue(key) { + const data = this._loadState(); + if (data && data.hasOwnProperty(key)) { + return data[key]; + } + return null; + } + + setValue(key, value) { + const data = this._loadState(); + data[key] = value; + this._globalState.update(this._stateKey, data); + } + +} diff --git a/src/project/indexer.js b/src/project/indexer.js new file mode 100644 index 0000000..8514793 --- /dev/null +++ b/src/project/indexer.js @@ -0,0 +1,221 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import { AUTO_REBUILD_DELAY } from '../constants'; +import { getCurrentPythonExecutable, isPioProject, runPioCommand, spawnCommand } from '../utils'; + +import path from 'path'; +import vscode from 'vscode'; + +export default class ProjectIndexer { + + constructor(projectPath) { + this.projectPath = projectPath; + + this.subscriptions = []; + this.libDirSubscriptions = new Map(); + + this.interval = null; + this.lastRebuildRequestedAt = null; + + this.isActive = false; + } + + async activate() { + this.isActive = true; + this.subscriptions = []; + await this.setup(); + } + + deactivate() { + this.isActive = false; + for (const subscription of this.subscriptions) { + subscription.dispose(); + } + this.subscriptions = []; + } + + dispose() { + this.deactivate(); + } + + async toggle() { + const config = vscode.workspace.getConfiguration('platformio-ide'); + const autoRebuildAutocompleteIndex = config.get('autoRebuildAutocompleteIndex'); + + if (this.isActive && !autoRebuildAutocompleteIndex) { + this.deactivate(); + } else if (!this.isActive && autoRebuildAutocompleteIndex) { + await this.activate(); + } + } + + async setup() { + await this.addProjectConfigWatcher(); + await this.updateLibDirsWatchers(); + this.requestIndexRebuild(); // FIXME: don't do this when user disables the corresponding setting + } + + addProjectConfigWatcher() { + try { + const watcher = vscode.workspace.createFileSystemWatcher( + path.join(this.projectPath, 'platformio.ini') + ); + this.subscriptions.push(watcher); + + this.subscriptions.push(watcher.onDidCreate(() => { + this.updateLibDirsWatchers(); + })); + this.subscriptions.push(watcher.onDidChange(() => { + this.requestIndexRebuild(); + this.updateLibDirsWatchers(); + })); + this.subscriptions.push(watcher.onDidDelete(() => { + this.updateLibDirsWatchers(); + })); + + } catch (error) { + console.log(error); + } + } + + async updateLibDirsWatchers() { + const libDirs = await this.fetchWatchDirs(); + + for (const newLibDir of libDirs.filter(libDirPath => !this.libDirSubscriptions.has(libDirPath))) { + await this.addLibDirWatcher(newLibDir); + } + + for (const removedLibDir of Array.from(this.libDirSubscriptions.keys()).filter(libDirPath => !libDirs.includes(libDirPath))) { + this.removeLibDirWatcher(removedLibDir); + } + } + + async addLibDirWatcher(libDirPath) { + try { + const watcher = vscode.workspace.createFileSystemWatcher( + path.join(libDirPath, '*') + ); + const subscription = watcher.onDidCreate(() => { + this.requestIndexRebuild(); + }); + + this.libDirSubscriptions.set(libDirPath, [watcher, subscription]); + + this.subscriptions.push(watcher); + this.subscriptions.push(subscription); + } catch (error) { + console.error(error); + } + } + + removeLibDirWatcher(libDirPath) { + const subscriptions = this.libDirSubscriptions.get(libDirPath) || []; + for (const s of subscriptions) { + if (s) { + this._removeSubscription(s); + s.dispose(); + } + } + } + + requestIndexRebuild() { + this.lastRebuildRequestedAt = new Date(); + if (this.interval === null) { + this.interval = setInterval(this.maybeRebuild.bind(this), AUTO_REBUILD_DELAY); + } + } + + async maybeRebuild() { + const now = new Date(); + if (now.getTime() - this.lastRebuildRequestedAt.getTime() > AUTO_REBUILD_DELAY) { + if (this.interval !== null) { + clearInterval(this.interval); + } + this.interval = null; + + if (this.isActive) { + await this.doRebuild(); + } + } + } + + doRebuild({ verbose = false } = {}) { + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: 'PlatformIO C/C++ Index Rebuild', + }, async (progress) => { + progress.report({ + message: 'Verifying if the current directory is a PlatformIO project', + }); + try { + if (!await isPioProject(this.projectPath)) { + return; + } + + progress.report({ + message: 'Performing index rebuild', + }); + await new Promise((resolve, reject) => { + runPioCommand(['init', '--project-dir', this.projectPath], (code, stdout, stderr) => { + if (code === 0) { + resolve(); + } else { + reject(stderr); + } + }); + }); + + if (verbose) { + vscode.window.showInformationMessage('PlatformIO: C/C++ Project Index (for Autocomplete, Linter) has been successfully rebuilt.'); + } + } catch (error) { + console.error(error); + vscode.window.showErrorMessage(`C/C++ project index rebuild failed: ${error.toString}`); + } + }); + } + + async fetchWatchDirs() { + if (!await isPioProject(this.projectPath)) { + return []; + } + const pythonExecutable = await getCurrentPythonExecutable(); + const script = [ + 'from os.path import join; from platformio import VERSION,util;', + 'print ":".join([', + ' join(util.get_home_dir(), "lib"),', + ' util.get_projectlib_dir(),', + ' util.get_projectlibdeps_dir()', + ']) if VERSION[0] == 3 else util.get_lib_dir()', + ].map(s => s.trim()).join(' '); + return new Promise((resolve, reject) => { + spawnCommand( + pythonExecutable, + ['-c', script], + (code, stdout, stderr) => { + if (code === 0) { + resolve(stdout.toString().trim().split(':')); + } else { + reject(stderr); + } + }, + { + spawnOptions: { + cwd: this.projectPath, + }, + } + ); + }); + } + + _removeSubscription(subscription) { + return this.subscriptions.splice(this.subscriptions.indexOf(subscription)); + } + +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..8cc1d7d --- /dev/null +++ b/src/utils.js @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2016-present, PlatformIO Plus + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import { DEFAULT_PIO_ARGS, ENV_BIN_DIR, IS_WINDOWS } from './constants'; + +import fs from 'fs-plus'; +import path from 'path'; +import spawn from 'cross-spawn'; +import vscode from 'vscode'; + +export function getCurrentPythonExecutable() { + const useBuiltinPIOCore = vscode.workspace.getConfiguration('platformio-ide').get('useBuiltinPIOCore'); + const customDirs = useBuiltinPIOCore ? [ENV_BIN_DIR] : null; + return getPythonExecutable(customDirs); +} + +export async function runPioCommand(args, callback, options = {}) { + spawnCommand( + await getCurrentPythonExecutable(), + ['-m', 'platformio', ...DEFAULT_PIO_ARGS, ...args], + callback, + options + ); +} + +export function isPioProject(dir) { + return fs.isFileSync(path.join(dir, 'platformio.ini')); +} + +export async function ensureDirExists(dirPath) { + if (!fs.isDirectorySync(dirPath)) { + fs.makeTreeSync(dirPath); + } +} + +export function getCoreVersion(pythonExecutable) { + return new Promise((resolve, reject) => { + spawnCommand( + pythonExecutable, + ['-m', 'platformio', '--version'], + (code, stdout, stderr) => { + if (code === 0) { + return resolve(stdout.trim().match(/[\d+\.]+.*$/)[0]); + } + return reject(stderr); + }, + { + cacheValid: '10s', + } + ); + }); +} + +export function spawnCommand(cmd, args, callback, options = {}) { + console.log('spawnCommand', cmd, args, options); + const completed = false; + const outputLines = []; + const errorLines = []; + + try { + const child = spawn(cmd, args, options.spawnOptions); + + child.stdout.on('data', (line) => outputLines.push(line)); + child.stderr.on('data', (line) => errorLines.push(line)); + child.on('close', onExit); + child.on('error', (err) => { + errorLines.push(err.toString()); + onExit(-1); + } + ); + } catch (error) { + errorLines.push(error.toString()); + onExit(-1); + } + + function onExit(code) { + if (completed) { + return; + } + + const stdout = outputLines.map(x => x.toString()).join(''); + const stderr = errorLines.map(x => x.toString()).join(''); + + callback(code, stdout, stderr); + } +} + +export async function getPythonExecutable(customDirs = null) { + const candidates = []; + const defaultName = IS_WINDOWS ? 'python.exe' : 'python'; + + if (customDirs) { + customDirs.forEach(dir => candidates.push(path.join(dir, defaultName))); + } + + if (IS_WINDOWS) { + candidates.push(defaultName); + candidates.push('C:\\Python27\\' + defaultName); + } else { + candidates.push('python2.7'); + candidates.push(defaultName); + } + + for (const item of process.env.PATH.split(path.delimiter)) { + if (fs.isFileSync(path.join(item, defaultName))) { + candidates.push(path.join(item, defaultName)); + } + } + + for (const executable of candidates) { + if ( (await isPython2(executable)) ) { + return executable; + } + } + + return null; +} + +function isPython2(executable) { + const args = ['-c', 'import sys; print \'.\'.join(str(v) for v in sys.version_info[:2])']; + return new Promise(resolve => { + spawnCommand( + executable, + args, + (code, stdout) => { + resolve(code === 0 && stdout.startsWith('2.7')); + } + ); + }); +}