diff --git a/.gitignore b/.gitignore index e51ebc5468..8fe91862fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .swp build dist +installer node_modules coverage npm-debug.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf4dbb202e..8fc1617a3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,13 +18,14 @@ Before you fil an issue or a pull request, quickly read of the following tips on ### Prerequisites Most of the time, you'll have installed Kitematic before contibuting, but for the -sake of completeness, you can also install [Node.js](https://nodejs.org/) and the latest Xcode from the Apple App Store and then run from your Git clone. +sake of completeness, you can also install [Node.js 0.10.38](https://nodejs.org/dist/v0.10.38/). -Running `npm start` will download and install the OS X Docker client, -[Docker machine](https://github.com/docker/machine), -the [Boot2Docker iso](https://github.com/boot2docker/boot2docker), -[Electron](http://electron.atom.io/), and [VirtualBox](https://www.virtualbox.org/) -if needed. +### Other Prerequisites (Mac) +- The latest Xcode from the Apple App Store. + +### Other Prerequisites (Windows) +- [Visual Studio 2013 Community](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) (or similar) +- [Python](https://www.python.org/downloads/release/python-2710/) ### Getting Started @@ -34,6 +35,11 @@ To run the app in development: - `npm start` +Running `npm start` will download and install the OS X Docker client, +[Docker Machine](https://github.com/docker/machine), [Docker Compose](https://github.com/docker/compose) +the [Boot2Docker iso](https://github.com/boot2docker/boot2docker), +[Electron](http://electron.atom.io/). + ### Building & Release - `npm run release` diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000000..4dedaf9122 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,272 @@ +var path = require('path'); +var execFile = require('child_process').execFile; +var packagejson = require('./package.json'); +var electron = require('electron-prebuilt'); + +var WINDOWS_DOCKER_URL = 'https://get.docker.com/builds/Windows/x86_64/docker-1.6.2.exe'; +var DARWIN_DOCKER_URL = 'https://get.docker.com/builds/Darwin/x86_64/docker-' + packagejson['docker-version']; +var WINDOWS_DOCKER_MACHINE_URL = 'https://github.com/docker/machine/releases/download/v' + packagejson['docker-machine-version'] + '/docker-machine_windows-amd64.exe'; +var DARWIN_DOCKER_MACHINE_URL = 'https://github.com/docker/machine/releases/download/v' + packagejson['docker-machine-version'] + '/docker-machine_darwin-amd64'; +var DARWIN_COMPOSE_URL = 'https://github.com/docker/compose/releases/download/' + packagejson['docker-compose-version'] + '/docker-compose-Darwin-x86_64'; +var BOOT2DOCKER_ISO_URL = 'https://github.com/boot2docker/boot2docker/releases/download/v' + packagejson['docker-version'] + '/boot2docker.iso'; + +module.exports = function (grunt) { + require('load-grunt-tasks')(grunt); + var target = grunt.option('target') || 'development'; + var beta = grunt.option('beta') || false; + var env = process.env; + env.NODE_ENV = target; + + var version = function (str) { + var match = str.match(/(\d+\.\d+\.\d+)/); + return match ? match[1] : null; + }; + + grunt.registerMultiTask('download-binary', 'Downloads binary unless version up to date', function () { + var target = grunt.task.current.target; + var done = this.async(); + var config = grunt.config('download-binary')[target]; + execFile(config.binary, ['--version'], function (err, stdout) { + var currentVersion = version(stdout); + if (!currentVersion || currentVersion !== version(config.version)) { + grunt.task.run('curl:' + target); + grunt.task.run('chmod'); + } + done(); + }); + }); + + var APPNAME = beta ? 'Kitematic (Beta)' : 'Kitematic'; + var OSX_OUT = './dist/osx'; + var OSX_FILENAME = OSX_OUT + '/' + APPNAME + '.app'; + + grunt.initConfig({ + IDENTITY: 'Developer ID Application: Docker Inc', + APPNAME: APPNAME, + OSX_OUT: OSX_OUT, + OSX_FILENAME: OSX_FILENAME, + OSX_FILENAME_ESCAPED: OSX_FILENAME.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)'), + + // electron + electron: { + windows: { + options: { + name: '<%= APPNAME %>', + dir: 'build/', + out: 'dist/', + version: packagejson['electron-version'], + platform: 'win32', + arch: 'x64', + asar: true + } + }, + osx: { + options: { + name: '<%= APPNAME %>', + dir: 'build/', + out: '<%= OSX_OUT %>', + version: packagejson['electron-version'], + platform: 'darwin', + arch: 'x64', + asar: true, + 'app-bundle-id': 'com.kitematic.kitematic' + } + } + }, + + 'create-windows-installer': { + appDirectory: 'dist/Kitematic-win32/', + outputDirectory: 'installer/', + authors: 'Docker Inc.' + }, + + // docker binaries + 'download-binary': { + docker: { + version: packagejson['docker-version'], + binary: path.join('resources', 'docker'), + download: 'curl:docker' + }, + 'docker-machine': { + version: packagejson['docker-machine-version'], + binary: path.join('resources', 'docker-machine'), + download: 'curl:docker-machine' + } + }, + + // images + copy: { + dev: { + files: [{ + expand: true, + cwd: '.', + src: ['package.json', 'settings.json', 'index.html'], + dest: 'build/' + }, { + expand: true, + cwd: 'images/', + src: ['**/*'], + dest: 'build/' + }, { + expand: true, + cwd: 'fonts/', + src: ['**/*'], + dest: 'build/' + }, { + cwd: 'node_modules/', + src: Object.keys(packagejson.dependencies).map(function (dep) { return dep + '/**/*';}), + dest: 'build/node_modules/', + expand: true + }] + }, + windows: { + files: [{ + expand: true, + cwd: 'resources', + src: ['docker*'], + dest: 'dist/Kitematic-win32/resources/resources/' + }], + options: { + mode: true + } + }, + osx: { + files: [{ + expand: true, + cwd: 'resources', + src: ['**/*'], + dest: '<%= OSX_FILENAME %>/Contents/Resources/resources/' + }, { + src: 'util/kitematic.icns', + dest: '<%= OSX_FILENAME %>/Contents/Resources/atom.icns' + }], + options: { + mode: true + } + } + }, + + // download binaries + curl: { + docker: { + src: process.platform === 'win32' ? WINDOWS_DOCKER_URL : DARWIN_DOCKER_URL, + dest: process.platform === 'win32' ? path.join('resources', 'docker.exe') : path.join('resources', 'docker') + }, + 'docker-machine': { + src: process.platform === 'win32' ? WINDOWS_DOCKER_MACHINE_URL : DARWIN_DOCKER_MACHINE_URL, + dest: process.platform === 'win32' ? path.join('resources', 'docker-machine.exe') : path.join('resources', 'docker-machine') + }, + 'docker-compose': { + src: DARWIN_COMPOSE_URL, + dest: 'resources/docker-compose' + }, + 'boot2docker-iso': { + src: BOOT2DOCKER_ISO_URL, + dest: path.join('resources', 'boot2docker-' + packagejson['docker-version']) + } + }, + + chmod: { + binaries: { + options: { + mode: '755' + }, + src: ['resources/docker*'] + } + }, + + // styles + less: { + options: { + sourceMapFileInline: true + }, + dist: { + files: { + 'build/main.css': 'styles/main.less' + } + } + }, + + // javascript + babel: { + options: { + sourceMap: 'inline', + blacklist: 'regenerator' + }, + dist: { + files: [{ + expand: true, + cwd: 'src/', + src: ['**/*.js'], + dest: 'build/', + }] + } + }, + + shell: { + electron: { + command: electron + ' ' + 'build', + options: { + async: true, + execOptions: { + env: env + } + } + }, + sign: { + options: { + failOnError: false, + }, + command: [ + 'codesign --deep -v -f -s "<%= IDENTITY %>" <%= OSX_FILENAME_ESCAPED %>/Contents/Frameworks/*', + 'codesign -v -f -s "<%= IDENTITY %>" <%= OSX_FILENAME_ESCAPED %>', + 'codesign -vvv --display <%= OSX_FILENAME_ESCAPED %>', + 'codesign -v --verify <%= OSX_FILENAME_ESCAPED %>', + ].join(' && '), + }, + zip: { + command: 'ditto -c -k --sequesterRsrc --keepParent <%= OSX_FILENAME_ESCAPED %> <%= OSX_OUT %>/Kitematic-' + packagejson.version + '.zip', + } + }, + + clean: { + dist: ['dist/'], + build: ['build/'] + }, + + // livereload + watchChokidar: { + options: { + spawn: false + }, + livereload: { + options: {livereload: true}, + files: ['build/**/*'] + }, + js: { + files: ['src/**/*.js'], + tasks: ['babel'] + }, + less: { + files: ['styles/**/*.less'], + tasks: ['less'] + }, + copy: { + files: ['images/*', 'index.html', 'fonts/*'], + tasks: ['copy'] + } + } + }); + grunt.registerTask('default', ['download-binary', 'babel', 'less', 'copy:dev', 'shell:electron', 'watchChokidar']); + + if (process.platform === 'win32') { + grunt.registerTask('release', ['clean:dist', 'clean:build', 'download-binary', 'babel', 'less', 'copy:dev', 'electron:windows', 'copy:windows', 'copy:osx']); + } else { + grunt.registerTask('release', ['clean:dist', 'clean:build', 'download-binary', 'babel', 'less', 'copy:dev', 'electron:osx', 'copy:osx', 'shell:sign', 'shell:zip']); + } + + process.on('SIGINT', function () { + grunt.task.run(['shell:electron:kill']); + process.exit(1); + }); +}; diff --git a/docs/index.md b/docs/index.md index 0ddfc9f3b9..60a58533b2 100755 --- a/docs/index.md +++ b/docs/index.md @@ -28,10 +28,9 @@ container! ## Technical Details -Kitematic is a self-contained .app, with a two exceptions: +Kitematic is a self-contained .app, with an exception: - It will install VirtualBox if it's not already installed. -- It copies the `docker` and `docker-machine` binaries to `/usr/local/bin` for convenience. ### Why does Kitematic need my root password? diff --git a/gulpfile.js b/gulpfile.js index 09b70a92b9..6099fb736b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -207,7 +207,7 @@ gulp.task('download-docker', function (cb) { } else { request('https://get.docker.com/builds/Darwin/x86_64/docker-' + packagejson['docker-version']) .pipe(fs.createWriteStream('./resources/docker')).on('finish', function () { - fs.chmodSync('./resources/docker', 755); + fs.chmodSync('./resources/docker', 0755); cb(); }); } @@ -230,7 +230,7 @@ gulp.task('download-docker-machine', function (cb) { } else { request('https://github.com/docker/machine/releases/download/v' + packagejson['docker-machine-version'] + '/docker-machine_darwin-amd64') .pipe(fs.createWriteStream('./resources/docker-machine')).on('finish', function () { - fs.chmodSync('./resources/docker-machine', 755); + fs.chmodSync('./resources/docker-machine', 0755); cb(); }); } @@ -252,7 +252,7 @@ gulp.task('download-docker-compose', function (cb) { gutil.log(gutil.colors.green('Downloading Docker Compose')); request('https://github.com/docker/compose/releases/download/' + packagejson['docker-compose-version'] + '/docker-compose-Darwin-x86_64') .pipe(fs.createWriteStream('./resources/docker-compose')).on('finish', function () { - fs.chmodSync('./resources/docker-compose', 755); + fs.chmodSync('./resources/docker-compose', 0755); cb(); }); } diff --git a/package.json b/package.json index f33814e197..202163f4fb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "author": "Kitematic", "description": "Simple Docker Container management for Mac OS X.", "homepage": "https://kitematic.com/", - "main": "build/browser.js", + "main": "browser.js", "repository": { "type": "git", "url": "git@github.com:kitematic/kitematic.git" @@ -14,8 +14,8 @@ "start": "gulp", "test": "jest -c jest-unit.json", "integration": "jest -c jest-integration.json", - "release": "gulp release", - "release:beta": "gulp release --beta", + "release": "grunt release", + "release:beta": "grunt release --beta=true", "lint": "jsxhint src", "reset": "gulp reset" }, @@ -26,7 +26,7 @@ } ], "docker-version": "1.6.2", - "docker-machine-version": "0.3.0-rc1", + "docker-machine-version": "0.3.0-rc2", "docker-compose-version": "1.2.0", "electron-version": "0.27.2", "virtualbox-version": "4.3.28", @@ -67,25 +67,28 @@ "devDependencies": { "babel": "^5.1.10", "babel-jest": "^5.2.0", - "gulp": "^3.8.11", - "gulp-babel": "^5.1.0", - "gulp-changed": "^1.2.1", - "gulp-concat": "^2.5.2", - "gulp-cssmin": "^0.1.6", - "gulp-download-electron": "^0.0.5", - "gulp-if": "^1.2.5", - "gulp-insert": "^0.4.0", - "gulp-less": "^3.0.2", - "gulp-livereload": "^3.8.0", - "gulp-plumber": "^1.0.0", - "gulp-shell": "^0.4.1", - "gulp-sourcemaps": "^1.5.2", - "gulp-util": "^3.0.4", + "electron-prebuilt": "^0.27.3", + "grunt": "^0.4.5", + "grunt-babel": "^5.0.1", + "grunt-chmod": "^1.0.3", + "grunt-cli": "^0.1.13", + "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-copy": "^0.8.0", + "grunt-contrib-less": "^1.0.1", + "grunt-contrib-watch-chokidar": "^1.0.0", + "grunt-curl": "^2.2.0", + "grunt-download-electron": "^2.1.1", + "grunt-electron": "^1.0.0", + "grunt-electron-installer": "^0.33.0", + "grunt-shell": "^1.1.2", + "grunt-shell-spawn": "^0.3.8", "jest-cli": "^0.4.5", "jsxhint": "^0.14.0", + "load-grunt-tasks": "^3.2.0", "minimist": "^1.1.1", "react-tools": "^0.13.1", "run-sequence": "^1.0.2", + "shell-escape": "^0.2.0", "source-map-support": "^0.2.10" } } diff --git a/src/browser.js b/src/browser.js index b731e338ef..cb8010dd1f 100644 --- a/src/browser.js +++ b/src/browser.js @@ -5,9 +5,8 @@ var fs = require('fs'); var ipc = require('ipc'); var path = require('path'); -process.env.NODE_PATH = path.join(__dirname, '/../node_modules'); +process.env.NODE_PATH = path.join(__dirname, 'node_modules'); process.env.RESOURCES_PATH = path.join(__dirname, '/../resources'); -process.chdir(path.join(__dirname, '..')); process.env.PATH = '/usr/local/bin:' + process.env.PATH; var size = {}, settingsjson = {}; @@ -15,9 +14,49 @@ try { size = JSON.parse(fs.readFileSync(path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], 'Library', 'Application\ Support', 'Kitematic', 'size'))); } catch (err) {} try { - settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); + settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, 'settings.json'), 'utf8')); } catch (err) {} +var handleStartupEvent = function() { + if (process.platform !== 'win32') { + return false; + } + + var squirrelCommand = process.argv[1]; + switch (squirrelCommand) { + case '--squirrel-install': + case '--squirrel-updated': + + // Optionally do things such as: + // + // - Install desktop and start menu shortcuts + // - Add your .exe to the PATH + // - Write to the registry for things like file associations and + // explorer context menus + + // Always quit when done + app.quit(); + + return true; + case '--squirrel-uninstall': + // Undo anything you did in the --squirrel-install and + // --squirrel-updated handlers + + // Always quit when done + app.quit(); + + return true; + case '--squirrel-obsolete': + // This is called on the outgoing version of your app before + // we update to the new version - it's the opposite of + // --squirrel-updated + app.quit(); + return true; + } +}; + +handleStartupEvent(); + var openURL = null; app.on('open-url', function (event, url) { event.preventDefault(); @@ -36,7 +75,7 @@ app.on('ready', function () { show: false, }); - mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'build/index.html'))); + mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, 'index.html'))); app.on('activate-with-no-open-windows', function () { if (mainWindow) { @@ -52,7 +91,8 @@ app.on('ready', function () { }); app.on('before-quit', function () { - if (!updating) { + // TODO: make this work for right click + close + if (!updating && mainWindow.webContents) { mainWindow.webContents.send('application:quitting'); } }); diff --git a/src/components/ContainerDetailsSubheader.react.js b/src/components/ContainerDetailsSubheader.react.js index 83a4390e54..42bda6c01c 100644 --- a/src/components/ContainerDetailsSubheader.react.js +++ b/src/components/ContainerDetailsSubheader.react.js @@ -103,8 +103,12 @@ var ContainerDetailsSubheader = React.createClass({ if (!this.disableTerminal()) { metrics.track('Terminaled Into Container'); var container = this.props.container; - var shell = ContainerUtil.env(container).SHELL; - if(typeof shell === 'undefined') { + var shell = ContainerUtil.env(container).reduce((envs, env) => { + envs[env[0]] = env[1]; + return envs; + }, {}).SHELL; + + if(!shell) { shell = 'sh'; } machine.ip().then(ip => { diff --git a/src/components/ContainerHomeFolders.react.js b/src/components/ContainerHomeFolders.react.js index b7d2c6d55f..5074bdf89e 100644 --- a/src/components/ContainerHomeFolders.react.js +++ b/src/components/ContainerHomeFolders.react.js @@ -25,7 +25,7 @@ var ContainerHomeFolder = React.createClass({ }, (index) => { if (index === 0) { var volumes = _.clone(this.props.container.Volumes); - var newHostVolume = path.join(util.home(), 'Kitematic', this.props.container.Name, containerVolume); + var newHostVolume = path.join(util.home(), util.documents(), 'Kitematic', this.props.container.Name, containerVolume); volumes[containerVolume] = newHostVolume; var binds = _.pairs(volumes).map(function (pair) { if(util.isWindows()) { @@ -58,7 +58,8 @@ var ContainerHomeFolder = React.createClass({ return false; } - var folders = _.map(this.props.container.Volumes, (val, key) => { + console.log(this.props.container.Volumes); + var folders = _.map(_.omit(this.props.container.Volumes, (v, k) => k.indexOf('/Users/') !== -1), (val, key) => { var firstFolder = key.split('/')[1]; return (