├── .gitignore ├── .gitattributes ├── coffeelint.json ├── package.json ├── LICENSE.md ├── README.md └── tasks ├── task-helpers.coffee └── build-atom-shell-task.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.swp 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_line_length": { 3 | "value": 140, 4 | "level": "warn" 5 | }, 6 | "no_empty_param_list": { 7 | "level": "error" 8 | }, 9 | "missing_fat_arrows": { 10 | "level": "warn" 11 | }, 12 | "arrow_spacing": { 13 | "level": "error" 14 | }, 15 | "no_interpolation_in_single_quotes": { 16 | "level": "error" 17 | }, 18 | "no_trailing_whitespace": { 19 | "level": "ignore" 20 | }, 21 | "no_debugger": { 22 | "level": "error" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-build-atom-shell", 3 | "description": "Grunt task to build atom-shell and rebuild node modules", 4 | "version": "2.1.1", 5 | "main": "grunt.js", 6 | "dependencies": { 7 | "cp-file": "^2.2.0", 8 | "fs-plus": "^2.3.2", 9 | "glob": "^5.0.6", 10 | "grunt": "0.4", 11 | "npm": "~1.4.5", 12 | "rx": "^2.3.20", 13 | "underscore": "^1.7.0" 14 | }, 15 | "licenses": [ 16 | { 17 | "type": "MIT", 18 | "url": "http://github.com/paulcbetts/grunt-build-atom-shell/raw/master/LICENSE.md" 19 | } 20 | ], 21 | "homepage": "https://github.com/paulcbetts/grunt-build-atom-shell", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/paulcbetts/grunt-build-atom-shell.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/paulcbetts/grunt-build-atom-shell/issues" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-build-atom-shell 2 | 3 | Build atom-shell from Git, and rebuild native modules. This is a mostly drop-in replacement for `grunt-download-atom-shell`, in that, you can replace your use of it with this package at the same point in the Atom build process and everything should Just Work. 4 | 5 | ## Why even would I do this? 6 | 7 | The main reason to do this is because of [atom/atom-shell#713](https://github.com/atom/atom-shell/issues/713) - trying to rename Atom after-the-fact isn't possible on Windows without some serious rigging. This package fixes that issue, as well as allows you to use arbitrary builds of Atom Shell (i.e. no more waiting for a new release for a bugfix). 8 | 9 | ## Installation 10 | 11 | Install npm package, next to your project's `Gruntfile.js` file: 12 | 13 | ```sh 14 | npm install --save-dev grunt-build-atom-shell 15 | ``` 16 | 17 | Add this line to your project's `Gruntfile.js`: 18 | 19 | ```js 20 | grunt.loadNpmTasks('grunt-build-atom-shell'); 21 | ``` 22 | 23 | ## Options 24 | 25 | * `buildDir` - **Required** Where to put the downloaded atom-shell 26 | * `tag` - **Required** A tag, branch, or commit of Atom Shell to build 27 | * `projectName` - **Required** A short name for your project (originally 'atom') 28 | * `productName` - **Required** The name of the final binary generated (originally 'Atom') 29 | * `targetDir` - Where to put the resulting atom-shell, defaults to ./atom-shell 30 | * `config` - Either 'Debug' or 'Release', defaults to 'Release' 31 | * `remoteUrl` - The Git remote url to download from, defaults to official Atom Shell 32 | * `nodeVersion` - The version of Node.js to use; see the section below for how to configure this 33 | 34 | ### Example 35 | 36 | #### Gruntfile.js 37 | 38 | ```js 39 | module.exports = function(grunt) { 40 | grunt.initConfig({ 41 | 'build-atom-shell': { 42 | tag: 'v0.19.5', 43 | nodeVersion: '0.18.0', 44 | buildDir: (process.env.TMPDIR || process.env.TEMP || '/tmp') + '/atom-shell', 45 | projectName: 'mycoolapp', 46 | productName: 'MyCoolApp' 47 | } 48 | }); 49 | }; 50 | ``` 51 | 52 | ### Correctly setting nodeVersion 53 | 54 | Different versions of Atom Shell expect to be linked against different versions of node.js. Since `grunt-build-atom-shell` allows you to use arbitrary commits of Atom Shell, there is no way for it to know which version is correct to use, so it must be explicitly provided. If you don't explicitly provide a version, we will guess the latest version, which may or may not be correct. 55 | 56 | * 0.19.x series - `0.18.0` 57 | * 0.20.x series - `0.20.0` 58 | * 0.21.x series - `0.21.0` 59 | * 0.22.x series - `0.22.0` 60 | 61 | These numbers **don't match the official node.js versions**, because they also reflect patches that Atom puts into node.js to make it compatible with Chromium. 62 | -------------------------------------------------------------------------------- /tasks/task-helpers.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs-plus' 2 | path = require 'path' 3 | cpFile = require 'cp-file' 4 | _ = require 'underscore' 5 | 6 | module.exports = (grunt) -> 7 | cp: (source, destination, {filter}={}) -> 8 | unless grunt.file.exists(source) 9 | grunt.fatal("Cannot copy non-existent #{source.cyan} to #{destination.cyan}") 10 | 11 | copyFile = (sourcePath, destinationPath) -> 12 | return if filter?(sourcePath) or filter?.test?(sourcePath) 13 | 14 | stats = fs.lstatSync(sourcePath) 15 | if stats.isSymbolicLink() 16 | grunt.file.mkdir(path.dirname(destinationPath)) 17 | fs.symlinkSync(fs.readlinkSync(sourcePath), destinationPath) 18 | else if stats.isFile() 19 | if stats.size < 64 * 1048576 20 | grunt.file.copy(sourcePath, destinationPath) 21 | else 22 | grunt.verbose.ok "Copying large file #{sourcePath} => #{destinationPath}" 23 | cpFile.sync sourcePath, destinationPath, overwrite: true 24 | 25 | if grunt.file.exists(destinationPath) 26 | fs.chmodSync(destinationPath, fs.statSync(sourcePath).mode) 27 | 28 | if grunt.file.isFile(source) 29 | copyFile(source, destination) 30 | else 31 | try 32 | onFile = (sourcePath) -> 33 | destinationPath = path.join(destination, path.relative(source, sourcePath)) 34 | copyFile(sourcePath, destinationPath) 35 | onDirectory = (sourcePath) -> 36 | if fs.isSymbolicLinkSync(sourcePath) 37 | destinationPath = path.join(destination, path.relative(source, sourcePath)) 38 | copyFile(sourcePath, destinationPath) 39 | false 40 | else 41 | true 42 | fs.traverseTreeSync source, onFile, onDirectory 43 | catch error 44 | grunt.fatal(error) 45 | 46 | grunt.verbose.writeln("Copied #{source.cyan} to #{destination.cyan}.") 47 | 48 | mkdir: (args...) -> 49 | grunt.file.mkdir(args...) 50 | 51 | rm: (args...) -> 52 | grunt.file.delete(args..., force: true) if grunt.file.exists(args...) 53 | 54 | spawn: (options, callback) -> 55 | childProcess = require 'child_process' 56 | stdout = (options.stdout && process.stdout) || [] 57 | stderr = (options.stderr && process.stderr) || [] 58 | error = null 59 | proc = childProcess.spawn(options.cmd, options.args, options.opts) 60 | proc.stdout.on 'data', (data) -> 61 | if _.isArray(stdout) 62 | stdout.push(data.toString()) 63 | else 64 | stdout.write(data.toString()) 65 | proc.stderr.on 'data', (data) -> 66 | if _.isArray(stderr) 67 | stderr.push(data.toString()) 68 | else 69 | stderr.write(data.toString()) 70 | proc.on 'error', (processError) -> error ?= processError 71 | proc.on 'close', (exitCode, signal) -> 72 | error ?= new Error(signal) if exitCode != 0 73 | results = {stderr: (if _.isArray(stderr) then stderr.join('') else ''), stdout: (if _.isArray(stdout) then stdout.join('') else ''), code: exitCode} 74 | grunt.log.error results.stderr if exitCode != 0 75 | callback(error, results, exitCode) 76 | -------------------------------------------------------------------------------- /tasks/build-atom-shell-task.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | rx = require 'rx' 4 | _ = require 'underscore' 5 | glob = require 'glob' 6 | 7 | module.exports = (grunt) -> 8 | {cp, mkdir, rm, spawn} = require('./task-helpers')(grunt) 9 | 10 | fixBorkedOSXPythonPath = (options) -> 11 | return options unless process.platform is 'darwin' 12 | paths = process.env.PATH.split(':') 13 | 14 | # NB: Atom Shell's build process requires PyObjC, which is part of system Python, 15 | # but not part of the Python installed by Homebrew. If the default Python is 16 | # the Homebrew one, we need to rig PATH to point to the good one 17 | pyPath = _.find paths, (x) -> fs.existsSync(path.join(x, 'python')) 18 | return options if pyPath is '/usr/bin' 19 | 20 | newEnv = _.extend {}, process.env 21 | 22 | ret = _.extend {}, options 23 | ret.opts ?= {} 24 | ret.opts.env ?= newEnv 25 | ret.opts.env.PATH = '/usr/bin:' + process.env.PATH 26 | 27 | ret.cmd = '/usr/bin/python' if ret.cmd is 'python' 28 | ret 29 | 30 | spawnObservable = (options={}) -> 31 | fixedOpts = fixBorkedOSXPythonPath(options) 32 | 33 | rx.Observable.create (subj) -> 34 | grunt.verbose.ok "Running: #{options.cmd} #{options.args.join ' '}" 35 | 36 | spawn fixedOpts, (error, result, code) -> 37 | if error 38 | subj.onError error 39 | return 40 | 41 | subj.onNext {error,result,code} 42 | subj.onCompleted() 43 | 44 | rx.Disposable.empty 45 | 46 | stripAllBinaries = (dirToStrip) -> 47 | if process.platform is 'win32' 48 | return rx.Observable.create (subj) -> 49 | symFiles = /\.(pdb|ilk|exp)$/i 50 | toDelete = _.filter glob.sync(path.join(dirToStrip, '**')), (x) -> x.match(symFiles) 51 | 52 | grunt.verbose.ok "Stripping symbols: #{toDelete.join(',')}" 53 | _.each toDelete, (x) -> rm(x) 54 | return rx.Observable.return(true).subscribe(subj) 55 | 56 | binaryFiles = /\.(so|dylib|node)$/i 57 | 58 | return rx.Observable.create((subj) -> 59 | grunt.verbose.ok "Stripping all binaries" 60 | 61 | binaries = _.filter(glob.sync(path.join(dirToStrip, '**')), (x) -> 62 | return true if x.match(binaryFiles) 63 | stat = fs.lstatSync(x) 64 | 65 | return false if stat.isSymbolicLink() 66 | 67 | # Check if executable bit is set 68 | return stat and stat.isFile() and (stat.mode & 0o111) > 0 69 | ) 70 | 71 | unless binaries and binaries.length > 0 72 | grunt.verbose.ok "Nothing to strip!" 73 | return rx.Observable.return(true).subscribe(subj) 74 | 75 | grunt.verbose.ok "Stripping binaries: #{binaries.join(',')}" 76 | 77 | return rx.Observable.fromArray(binaries) 78 | .flatMap((x) -> spawnObservable({cmd: 'strip', args: [x], opts: {cwd: dirToStrip }}).catch(rx.Observable.return(true))) 79 | .takeLast(1) 80 | .subscribe(subj) 81 | ) 82 | 83 | bootstrapAtomShell = (buildDir, atomShellDir, remoteUrl, tag, stdout, stderr) -> 84 | cmds = [ 85 | { cmd: 'git', args: ['fetch', 'origin'], opts: {cwd: atomShellDir}, stdout: stdout, stderr: stderr }, 86 | { cmd: 'git', args: ['reset', '--hard', 'HEAD'], opts: {cwd: atomShellDir}, stdout: stdout, stderr: stderr }, 87 | { cmd: 'git', args: ['checkout', tag, ], opts: {cwd: atomShellDir}, stdout: stdout, stderr: stderr }, 88 | ] 89 | 90 | if fs.existsSync(atomShellDir) 91 | cmds.unshift { cmd: 'git', args: ['remote', 'set-url', 'origin', remoteUrl], opts: {cwd: atomShellDir} }, 92 | else 93 | rm atomShellDir 94 | 95 | grunt.verbose.ok "Cloning to #{buildDir}" 96 | grunt.file.mkdir buildDir 97 | cmds.unshift { cmd: 'git', args: ['clone', remoteUrl, 'atom-shell'], opts: {cwd: buildDir} }, 98 | 99 | bootstrapAtomShell = rx.Observable.fromArray(cmds) 100 | .concatMap (x) -> spawnObservable(x) 101 | .takeLast(1) 102 | 103 | envWithGypDefines = (projectName, productName) -> 104 | ewg = _.extend {}, process.env 105 | ewg.GYP_DEFINES = "project_name=#{projectName} product_name=#{productName.replace(' ','\\ ')}" 106 | if process.env.GYP_DEFINES? 107 | ewg.GYP_DEFINES = "#{process.env.GYP_DEFINES} #{ewg.GYP_DEFINES}" 108 | ewg 109 | 110 | buildAtomShell = (atomShellDir, config, projectName, productName, forceRebuild, stdout, stderr, arch) -> 111 | cmdOptions = 112 | cwd: atomShellDir 113 | env: envWithGypDefines(projectName, productName) 114 | 115 | bootstrapCmd = 116 | cmd: 'python' 117 | args: ['script/bootstrap.py', '-v'] 118 | opts: cmdOptions 119 | stdout: stdout 120 | stderr: stderr 121 | 122 | buildCmd = 123 | cmd: 'python' 124 | args: ['script/build.py', '-c', config, '-t', projectName] 125 | opts: cmdOptions 126 | stdout: stdout 127 | stderr: stderr 128 | 129 | if arch 130 | bootstrapCmd.args.push '--target_arch' 131 | bootstrapCmd.args.push arch 132 | 133 | rx.Observable.create (subj) -> 134 | grunt.verbose.ok "Rigging atom.gyp to have correct name" 135 | gypFile = path.join(atomShellDir, 'atom.gyp') 136 | atomGyp = grunt.file.read gypFile 137 | atomGyp = atomGyp 138 | .replace("'project_name': 'atom'", "'project_name': '#{projectName}'") 139 | .replace("'product_name': 'Atom'", "'product_name': '#{productName}'") 140 | .replace("'framework_name': 'Atom Framework'", "'framework_name': '#{productName} Framework'") 141 | .replace("'<(project_name) Framework'", "'<(product_name) Framework'") # fix upstream typo in 0.20.3 142 | 143 | grunt.file.write gypFile, atomGyp 144 | 145 | canary = path.join(atomShellDir, 'vendor', 'brightray', 'vendor', 'libchromiumcontent', 'VERSION') 146 | outDir = path.join(atomShellDir, 'out', 'R') 147 | 148 | bootstrap = spawnObservable(bootstrapCmd) 149 | if fs.existsSync(canary) and fs.existsSync(outDir) and (not forceRebuild?) 150 | grunt.verbose.ok("bootstrap appears to have been run, skipping it to save time!") 151 | bootstrap = rx.Observable.return(true) 152 | 153 | rx.Observable.concat(bootstrap, spawnObservable(buildCmd), stripAllBinaries(outDir)) 154 | .takeLast(1) 155 | .subscribe(subj) 156 | 157 | generateNodeLib = (atomShellDir, config, projectName, forceRebuild, nodeVersion, stdout, stderr, arch) -> 158 | return rx.Observable.return(true) unless process.platform is 'win32' 159 | 160 | homeDir = if process.platform is 'win32' then process.env.USERPROFILE else process.env.HOME 161 | atomHome = process.env.ATOM_HOME ? path.join(homeDir, ".#{projectName}") 162 | nodeGypHome = path.join(atomHome, '.node-gyp') 163 | 164 | rx.Observable.create (subj) -> 165 | source = path.resolve atomShellDir, 'out', 'R', 'node.dll.lib' 166 | target = path.resolve nodeGypHome, '.node-gyp', nodeVersion, arch ? 'ia32', 'node.lib' 167 | 168 | grunt.verbose.ok 'Copying new node.lib' 169 | if fs.existsSync(source) and (not forceRebuild?) 170 | grunt.verbose.ok 'Found existing node.lib, reusing it' 171 | cp source, target 172 | return rx.Observable.return(true).subscribe(subj) 173 | 174 | buildNodeLib = 175 | cmd: 'python' 176 | args: ['script/build.py', '-c', config, '-t', 'generate_node_lib'] 177 | opts: { cwd: atomShellDir } 178 | stdout: stdout 179 | stderr: stderr 180 | 181 | rx.Observable.return(true).do(-> cp source, target).subscribe(subj) 182 | 183 | installNode = (projectName, nodeVersion, stdout, stderr, arch) -> 184 | nodeArch = arch ? switch process.platform 185 | when 'darwin' then 'x64' 186 | when 'win32' then 'ia32' 187 | else process.arch 188 | 189 | homeDir = if process.platform is 'win32' then process.env.USERPROFILE else process.env.HOME 190 | atomHome = process.env.ATOM_HOME ? path.join(homeDir, ".#{projectName}") 191 | nodeGypHome = path.join(atomHome, '.node-gyp') 192 | distUrl = process.env.ATOM_NODE_URL ? 'https://gh-contractor-zcbenz.s3.amazonaws.com/atom-shell/dist' 193 | 194 | canary = path.join(nodeGypHome, '.node-gyp', nodeVersion, 'common.gypi') 195 | if (fs.existsSync(canary)) 196 | return rx.Observable.create (subj) -> 197 | grunt.verbose.ok 'Found existing node.js installation, skipping install to save time!' 198 | rx.Observable.return(true).subscribe(subj) 199 | 200 | cmd = 'node' 201 | args = [require.resolve('npm/node_modules/node-gyp/bin/node-gyp'), 'install', 202 | "--target=#{nodeVersion}", 203 | "--arch=#{nodeArch}", 204 | "--dist-url=#{distUrl}"] 205 | 206 | env = _.extend {}, process.env, HOME: nodeGypHome 207 | env.USERPROFILE = env.HOME if process.platform is 'win32' 208 | 209 | rx.Observable.create (subj) -> 210 | grunt.verbose.ok 'Installing node.js' 211 | spawnObservable({cmd, args, opts: {env}, stdout: stdout, stderr: stderr}).subscribe(subj) 212 | 213 | rebuildNativeModules = (projectName, nodeVersion, stdout, stderr, arch) -> 214 | nodeArch = arch ? switch process.platform 215 | when 'darwin' then 'x64' 216 | when 'win32' then 'ia32' 217 | else process.arch 218 | 219 | homeDir = if process.platform is 'win32' then process.env.USERPROFILE else process.env.HOME 220 | atomHome = process.env.ATOM_HOME ? path.join(homeDir, ".#{projectName}") 221 | nodeGypHome = path.join(atomHome, '.node-gyp') 222 | 223 | cmd = 'node' 224 | args = [require.resolve('npm/bin/npm-cli'), 'rebuild', "--target=#{nodeVersion}", "--arch=#{nodeArch}"] 225 | env = _.extend {}, process.env, HOME: nodeGypHome 226 | env.USERPROFILE = env.HOME if process.platform is 'win32' 227 | 228 | rx.Observable.create (subj) -> 229 | grunt.verbose.ok 'Rebuilding native modules against Atom Shell' 230 | 231 | spawnObservable({cmd, args, opts: {env}, stdout: stdout, stderr: stderr}) 232 | .concat(stripAllBinaries('../node_modules')) 233 | .subscribe(subj) 234 | 235 | grunt.registerTask 'rebuild-native-modules', "Rebuild native modules (debugging)", -> 236 | done = @async() 237 | 238 | {buildDir, config, projectName, nodeVersion, stdout, stderr, arch} = grunt.config 'build-atom-shell' 239 | config ?= 'Release' 240 | atomShellDir = path.join buildDir, 'atom-shell' 241 | 242 | rebuild = rx.Observable.concat( 243 | installNode(projectName, nodeVersion, stdout, stderr, arch), 244 | generateNodeLib(atomShellDir, config, projectName, true, nodeVersion, stdout, stderr, arch), 245 | rebuildNativeModules(projectName, nodeVersion, stdout, stderr, arch)).takeLast(1) 246 | 247 | rebuild.subscribe(done, done) 248 | 249 | grunt.registerTask 'rebuild-atom-shell', 'Clean build Atom Shell', -> 250 | @requiresConfig "build-atom-shell.buildDir" 251 | 252 | {buildDir} = grunt.config 'build-atom-shell' 253 | atomShellDir = path.join buildDir, 'atom-shell' 254 | rm atomShellDir 255 | 256 | grunt.task.run 'build-atom-shell' 257 | 258 | grunt.registerTask 'build-atom-shell', 'Build Atom Shell from source', -> 259 | done = @async() 260 | 261 | @requiresConfig "#{@name}.buildDir", "#{@name}.tag", "#{@name}.projectName", "#{@name}.productName" 262 | 263 | {buildDir, targetDir, config, remoteUrl, projectName, productName, tag, forceRebuild, nodeVersion, arch, stdout, stderr} = grunt.config @name 264 | config ?= 'Release' 265 | remoteUrl ?= 'https://github.com/atom/atom-shell' 266 | targetDir ?= 'atom-shell' 267 | atomShellDir = path.join buildDir, 'atom-shell' 268 | nodeVersion ?= process.env.ATOM_NODE_VERSION ? '0.20.0' 269 | 270 | buildAndTryBootstrappingIfItDoesntWork = 271 | buildAtomShell(atomShellDir, config, projectName, productName, forceRebuild, stdout, stderr, arch) 272 | .catch(buildAtomShell(atomShellDir, config, projectName, productName, true, stdout, stderr, arch)) 273 | 274 | buildErrything = rx.Observable.concat( 275 | bootstrapAtomShell(buildDir, atomShellDir, remoteUrl, tag, stdout, stderr), 276 | buildAndTryBootstrappingIfItDoesntWork, 277 | installNode(projectName, nodeVersion, stdout, stderr, arch), 278 | generateNodeLib(atomShellDir, config, projectName, forceRebuild, nodeVersion, stdout, stderr, arch), 279 | rebuildNativeModules(projectName, nodeVersion, stdout, stderr, arch)).takeLast(1) 280 | 281 | buildErrything 282 | .map (x) -> 283 | rm targetDir 284 | cp(path.resolve(atomShellDir, 'out', config.slice(0,1)), targetDir) 285 | return x 286 | .subscribe(( ->), done, done) 287 | --------------------------------------------------------------------------------