diff --git a/__tests__/commands/self-update.js b/__tests__/commands/self-update.js deleted file mode 100644 index 1361dd8dfb..0000000000 --- a/__tests__/commands/self-update.js +++ /dev/null @@ -1,118 +0,0 @@ -/* @flow */ - -import roadrunner from 'roadrunner'; -import BlockingQueue from '../../src/util/blocking-queue.js'; -import * as child from '../../src/util/child.js'; -import Config from '../../src/config.js'; -import {SELF_UPDATE_DOWNLOAD_FOLDER, CACHE_FILENAME} from '../../src/constants.js'; -import * as fs from '../../src/util/fs.js'; -import {run as selfUpdate} from '../../src/cli/commands/self-update.js'; -import * as reporters from '../../src/reporters/index.js'; - -const path = require('path'); -const stream = require('stream'); -// these tests touch "updates" folder and must be run in sequence -const queue = new BlockingQueue('self-update-tests', 1); - -jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; -const updatesFolder = path.resolve(__dirname, '..', '..', SELF_UPDATE_DOWNLOAD_FOLDER); -const updateTempFolder = path.resolve(__dirname, '..', '..', 'updates_temp'); - -declare function beforeAll(fn: Function): void; -declare function afterAll(fn: Function): void; - -beforeAll(async (done) => { - if (await fs.exists(updatesFolder)) { - await fs.rename(updatesFolder, updateTempFolder); - } - done(); -}); - -afterAll(async (done) => { - if (await fs.exists(updateTempFolder)) { - await fs.rename(updateTempFolder, updatesFolder); - } - done(); -}); - -let queueCounter = 0; -function run(checks: (reporter: reporters.Reporter, config: Config) => Promise): Promise { - return queue.push(`${queueCounter++}`, async () => { - let out = ''; - const stdout = new stream.Writable({ - decodeStrings: false, - write(data, encoding, cb) { - out += data; - cb(); - }, - }); - try { - const reporter = new reporters.ConsoleReporter({stdout, stderr: stdout}); - const cwd = path.resolve(updatesFolder, '..'); - const config = new Config(reporter); - await config.init({cwd}); - roadrunner.reset(CACHE_FILENAME); - await checks(reporter, config); - } catch (err) { - throw new Error(`${err} \nConsole output:\n ${out}`); - } finally { - roadrunner.reset(CACHE_FILENAME); - await fs.unlink(updatesFolder); - } - }); -} - -xit('Self-update should download a release and symlink it as "current"', (): Promise => { - return run(async (reporter, config) => { - await selfUpdate(config, reporter, {version: () => '0.0.1'}, []); - - const currentFolder = path.resolve(updatesFolder, 'current'); - expect(await fs.exists(currentFolder)).toBe(true); - const packageJson = await fs.readJson(path.resolve(updatesFolder, 'current', 'package.json')); - - expect(await fs.exists(await fs.realpath(currentFolder))).toBe(true); - const version = await child.exec('node bin/yarn.js -V'); - - expect(version[0].trim()).toBe(packageJson.version); - }); -}); - - -xit('Self-update should work from self-updated location', (): Promise => { - return run(async (reporter, config) => { - // mock an existing self-update - await child.exec('npm run build'); - const versionFolder = path.resolve(updatesFolder, '0.2.0'); - await fs.copy(path.resolve(updatesFolder, '..'), versionFolder); - await fs.symlink(versionFolder, path.resolve(updatesFolder, 'current')); - let packageJson = await fs.readJson(path.resolve(updatesFolder, 'current', 'package.json')); - packageJson.version = '0.2.0'; - await fs.writeFile(path.resolve(updatesFolder, 'current', 'package.json'), - JSON.stringify(packageJson, null, 4)); - let version = await child.exec('node bin/yarn.js -V'); - expect(version[0].trim()).toBe('0.2.0'); - - // mock a to_clean folder - packageJson.version = '0.1.0'; - fs.mkdirp(path.resolve(updatesFolder, '0.1.0')); - await fs.symlink(path.resolve(updatesFolder, '0.1.0'), path.resolve(updatesFolder, 'to_clean')); - await fs.writeFile(path.resolve(updatesFolder, '0.1.0', 'package.json'), - JSON.stringify(packageJson, null, 4)); - - await child.exec('node bin/yarn.js self-update'); - - // new version is current - version = await child.exec('node bin/yarn.js -V'); - const currentFolder = path.resolve(updatesFolder, 'current'); - expect(await fs.exists(currentFolder)).toBe(true); - packageJson = await fs.readJson(path.resolve(updatesFolder, 'current', 'package.json')); - expect(version[0].trim()).toBe(packageJson.version); - - expect(await fs.exists(path.resolve(updatesFolder, '0.1.0'))).toBe(false); - expect(await fs.exists(path.resolve(updatesFolder, '0.2.0'))).toBe(true); - expect(await fs.exists(await fs.realpath(currentFolder))).toBe(true); - - packageJson = await fs.readJson(path.resolve(updatesFolder, 'to_clean', 'package.json')); - expect(packageJson.version).toBe('0.2.0'); - }); -}, 180000); // 3 minutes diff --git a/circle.yml b/circle.yml index 096979d2dc..e2c594d084 100644 --- a/circle.yml +++ b/circle.yml @@ -51,6 +51,7 @@ deployment: commands: - ~/ghr/ghr --username yarnpkg --repository yarn --token $KPM_CIRCLE_RELEASE_TOKEN v$(dist/bin/yarn --version) artifacts/ - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc + - ./scripts/set-installation-method.js "`pwd`/package.json" npm - npm publish notify: diff --git a/package.json b/package.json index 9a97c1a047..7018961112 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "inquirer": "^1.2.2", "invariant": "^2.2.0", "is-builtin-module": "^1.0.0", - "is-ci": "^1.0.9", + "is-ci": "^1.0.10", "leven": "^2.0.0", "loud-rejection": "^1.2.0", "minimatch": "^3.0.3", diff --git a/scripts/install-latest.sh b/scripts/install-latest.sh index de72dce7c8..2d614e7a55 100755 --- a/scripts/install-latest.sh +++ b/scripts/install-latest.sh @@ -106,9 +106,25 @@ yarn_install() { printf "${white}Installing Yarn!$reset\n" if [ -d "$HOME/.yarn" ]; then - printf "$red> ~/.yarn already exists, possibly from a past Yarn install.$reset\n" - printf "$red> Remove it (rm -rf ~/.yarn) and run this script again.$reset\n" - exit 0 + if [ -n `which yarn` ]; then + if [ "$1" = '--nightly' ]; then + latest_url=https://nightly.yarnpkg.com/latest-tar-version + else + latest_url=https://yarnpkg.com/latest-version + fi + LATEST_VERSION=`curl $latest_url` + YARN_VERSION=`yarn -V` + + if [ "$LATEST_VERSION" -eq "$YARN_VERSION" ]; then + printf "$green> Yarn is already at the latest version.$reset\n" + else + rm -rf "$HOME/.yarn" + fi + else + printf "$red> ~/.yarn already exists, possibly from a past Yarn install.$reset\n" + printf "$red> Remove it (rm -rf ~/.yarn) and run this script again.$reset\n" + exit 0 + fi fi yarn_get_tarball $1 diff --git a/src/cli/commands/index.js b/src/cli/commands/index.js index bdb83ec696..ae8e4cc492 100644 --- a/src/cli/commands/index.js +++ b/src/cli/commands/index.js @@ -23,7 +23,6 @@ import * as pack from './pack.js'; export {pack}; import * as publish from './publish.js'; export {publish}; import * as remove from './remove.js'; export {remove}; import * as run from './run.js'; export {run}; -import * as selfUpdate from './self-update.js'; export {selfUpdate}; import * as tag from './tag.js'; export {tag}; import * as team from './team.js'; export {team}; import * as unlink from './unlink.js'; export {unlink}; diff --git a/src/cli/commands/install.js b/src/cli/commands/install.js index 3ec7c82be1..78838a59d0 100644 --- a/src/cli/commands/install.js +++ b/src/cli/commands/install.js @@ -26,8 +26,15 @@ import map from '../../util/map.js'; import {sortAlpha} from '../../util/misc.js'; const invariant = require('invariant'); +const userHome = require('user-home'); +const semver = require('semver'); const emoji = require('node-emoji'); +const isCI = require('is-ci'); const path = require('path'); +const fs2 = require('fs'); + +const {verison: YARN_VERSION, installationMethod: YARN_INSTALL_METHOD} = require('../../../package.json'); +const ONE_DAY = 1000 * 60 * 60 * 24; export type InstallCwdRequest = [ DependencyRequestPatterns, @@ -64,6 +71,47 @@ type Flags = { tilde: boolean, }; +/** + * Try and detect the installation method for Yarn and provide a command to update it with. + */ + +function getUpdateCommand(): ?string { + if (YARN_INSTALL_METHOD === 'tar') { + return 'curl -o- -L https://yarnpkg.com/install.sh | bash'; + } + + if (YARN_INSTALL_METHOD === 'homebrew') { + return 'brew upgrade yarn'; + } + + if (YARN_INSTALL_METHOD === 'deb') { + return 'sudo apt-get update && sudo apt-get install yarn'; + } + + if (YARN_INSTALL_METHOD === 'rpm') { + return 'sudo yum install yarn'; + } + + if (YARN_INSTALL_METHOD === 'npm') { + return 'npm upgrade --global yarn'; + } + + if (YARN_INSTALL_METHOD === 'chocolatey') { + return 'choco upgrade yarn'; + } + + return null; +} + +function getUpdateInstaller(): ?string { + // Windows + if (YARN_INSTALL_METHOD === 'msi') { + return 'https://yarnpkg.com/latest.msi'; + } + + return null; +} + function normalizeFlags(config: Config, rawFlags: Object): Flags { const flags = { // install @@ -277,6 +325,8 @@ export class Install { */ async init(): Promise> { + this.checkUpdate(); + // warn if we have a shrinkwrap if (await fs.exists(path.join(this.config.cwd, 'npm-shrinkwrap.json'))) { this.reporter.error(this.reporter.lang('shrinkwrapWarning')); @@ -354,6 +404,7 @@ export class Install { // fin! await this.saveLockfileAndIntegrity(patterns); + this.maybeOutputUpdate(); this.config.requestManager.clearCache(); return patterns; } @@ -634,6 +685,72 @@ export class Install { return request; } + + /** + * Check for updates every day and output a nag message if there's a newer version. + */ + + checkUpdate() { + if (!process.stdout.isTTY || isCI) { + // don't show upgrade dialog on CI or non-TTY terminals + return; + } + + // only check for updates once a day + const lastUpdateCheck = Number(this.config.getOption('lastUpdateCheck')) || 0; + if (lastUpdateCheck && Date.now() - lastUpdateCheck < ONE_DAY) { + return; + } + + // don't bug for updates on tagged releases + if (YARN_VERSION.indexOf('-') >= 0) { + return; + } + + this._checkUpdate().catch(() => { + // swallow errors + }); + } + + async _checkUpdate(): Promise { + let latestVersion = await this.config.requestManager.request({ + url: 'https://yarnpkg.com/latest-version', + }); + invariant(typeof latestVersion === 'string', 'expected string'); + latestVersion = latestVersion.trim(); + if (!semver.valid(latestVersion)) { + return; + } + + // ensure we only check for updates periodically + this.config.registries.yarn.saveHomeConfig({ + lastUpdateCheck: Date.now(), + }); + + if (semver.gt(latestVersion, YARN_VERSION)) { + this.maybeOutputUpdate = () => { + this.reporter.warn(this.reporter.lang('yarnOutdated', latestVersion, YARN_VERSION)); + + const command = getUpdateCommand(); + if (command) { + this.reporter.info(this.reporter.lang('yarnOutdatedCommand')); + this.reporter.command(command); + } else { + const installer = getUpdateInstaller(); + if (installer) { + this.reporter.info(this.reporter.lang('yarnOutdatedInstaller', installer)); + } + } + }; + } + } + + /** + * Method to override with a possible upgrade message. + */ + + maybeOutputUpdate() {} + maybeOutputUpdate: any; } export function setFlags(commander: Object) { diff --git a/src/cli/commands/self-update.js b/src/cli/commands/self-update.js deleted file mode 100644 index 49df912a7a..0000000000 --- a/src/cli/commands/self-update.js +++ /dev/null @@ -1,83 +0,0 @@ -/* @flow */ - -import roadrunner from 'roadrunner'; -import semver from 'semver'; -import path from 'path'; -import type Config from '../../config.js'; -import { - CACHE_FILENAME, - SELF_UPDATE_DOWNLOAD_FOLDER, - SELF_UPDATE_TARBALL_URL, - SELF_UPDATE_VERSION_URL, -} from '../../constants.js'; -import TarballFetcher from '../../fetchers/tarball-fetcher.js'; -import type {Reporter} from '../../reporters/index.js'; -import {exists, realpath, symlink, unlink} from '../../util/fs.js'; - -export const noArguments = true; -export const requireLockfile = false; - -export async function run( - config: Config, - reporter: Reporter, - flags: Object, - args: Array, -): Promise { - const currentVersion = flags.version(); - const latestVersion = await config.requestManager.request({ - url: SELF_UPDATE_VERSION_URL, - headers: { - 'Accept': 'text/plain', - }, - }); - - // Check if we already use the latest or a newer version - if (semver.compare(currentVersion, latestVersion) >= 0) { - reporter.success(reporter.lang('selfUpdateNoNewer')); - return; - } - - reporter.info(reporter.lang('selfUpdateDownloading', latestVersion)); - - const thisVersionRoot = path.resolve(__dirname, '..', '..', '..'); - let updatesFolder = path.resolve(thisVersionRoot, '..'); - const isCurrentVersionAnUpdate = path.basename(updatesFolder) === SELF_UPDATE_DOWNLOAD_FOLDER; - - if (!isCurrentVersionAnUpdate) { - updatesFolder = path.resolve(thisVersionRoot, SELF_UPDATE_DOWNLOAD_FOLDER); - } - - const locToUnzip = path.resolve(updatesFolder, latestVersion); - - await unlink(locToUnzip); - - const fetcher = new TarballFetcher(locToUnzip, { - type: 'tarball', - registry: 'yarn', - reference: SELF_UPDATE_TARBALL_URL, - hash: null, - }, config, false); - await fetcher.fetch(); - - // this links the downloaded release to bin/yarn.js - await symlink(locToUnzip, path.resolve(updatesFolder, 'current')); - - // clean garbage - const pathToClean = path.resolve(updatesFolder, 'to_clean'); - if (await exists(pathToClean)) { - const previousVersionToCleanup = await realpath(pathToClean); - await unlink(previousVersionToCleanup); - await unlink(pathToClean); - } - - if (isCurrentVersionAnUpdate) { - // current yarn installation is an update, let's clean it next time an update is run - // because it may still be in use now - await symlink(thisVersionRoot, pathToClean); - } - - // reset the roadrunner cache - roadrunner.reset(CACHE_FILENAME); - - reporter.success(reporter.lang('selfUpdateReleased', latestVersion)); -} diff --git a/src/config.js b/src/config.js index 321e4128bb..1abb773da5 100644 --- a/src/config.js +++ b/src/config.js @@ -398,6 +398,7 @@ export default class Config { return file; } } + return null; }); } diff --git a/src/reporters/lang/en.js b/src/reporters/lang/en.js index 00321371c5..e64b840dd0 100644 --- a/src/reporters/lang/en.js +++ b/src/reporters/lang/en.js @@ -77,6 +77,10 @@ const messages = { jsonError: 'Error parsing JSON at $0, $1.', noFilePermission: "We don't have permissions to touch the file $0.", + yarnOutdated: "Your current version of Yarn is out of date. The latest version is $0 while you're on $1.", + yarnOutdatedInstaller: 'To upgrade, download the latest installer at $0.', + yarnOutdatedCommand: 'To upgrade, run the following command:', + tooManyArguments: 'Too many arguments, maximum of $0.', tooFewArguments: 'Not enough arguments, expected at least $0.', noArguments: "This command doesn't require any arguments.", diff --git a/yarn.lock b/yarn.lock index 5a7a15d50e..8064faad76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,5 +1,7 @@ # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 + + abab@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" @@ -1329,6 +1331,10 @@ chokidar@^1.4.3, chokidar@^1.5.2: optionalDependencies: fsevents "^1.0.0" +ci-info@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.0.0.tgz#dc5285f2b4e251821683681c381c3388f46ec534" + cipher-base@^1.0.0, cipher-base@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07" @@ -2698,7 +2704,13 @@ is-builtin-module@^1.0.0: dependencies: builtin-modules "^1.0.0" -is-ci, is-ci@^1.0.9: +is-ci: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" + dependencies: + ci-info "^1.0.0" + +is-ci@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.9.tgz#de2c5ffe49ab3237fda38c47c8a3bbfd55bbcca7" @@ -5245,4 +5257,3 @@ yargs@~3.27.0: os-locale "^1.4.0" window-size "^0.1.2" y18n "^3.2.0" -