1/*
2 * Jake JavaScript build tool
3 * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *         http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17*/
18
19let path = require('path');
20let fs = require('fs');
21let exec = require('child_process').exec;
22let FileList = require('filelist').FileList;
23
24/**
25  @name jake
26  @namespace jake
27*/
28/**
29  @name jake.PackageTask
30  @constructor
31  @description Instantiating a PackageTask creates a number of Jake
32  Tasks that make packaging and distributing your software easy.
33
34  @param {String} name The name of the project
35  @param {String} version The current project version (will be
36  appended to the project-name in the package-archive
37  @param {Function} definition Defines the contents of the package,
38  and format of the package-archive. Will be executed on the instantiated
39  PackageTask (i.e., 'this', will be the PackageTask instance),
40  to set the various instance-propertiess.
41
42  @example
43  let t = new jake.PackageTask('rous', 'v' + version, function () {
44    let files = [
45      'Capfile'
46    , 'Jakefile'
47    , 'README.md'
48    , 'package.json'
49    , 'app/*'
50    , 'bin/*'
51    , 'config/*'
52    , 'lib/*'
53    , 'node_modules/*'
54    ];
55    this.packageFiles.include(files);
56    this.packageFiles.exclude('node_modules/foobar');
57    this.needTarGz = true;
58  });
59
60 */
61let PackageTask = function () {
62  let args = Array.prototype.slice.call(arguments);
63  let name = args.shift();
64  let version = args.shift();
65  let definition = args.pop();
66  let prereqs = args.pop() || []; // Optional
67
68  prereqs = [].concat(prereqs); // Accept string or list
69
70  /**
71    @name jake.PackageTask#name
72    @public
73    @type {String}
74    @description The name of the project
75   */
76  this.name = name;
77  /**
78    @name jake.PackageTask#version
79    @public
80    @type {String}
81    @description The project version-string
82   */
83  this.version = version;
84  /**
85    @name jake.PackageTask#prereqs
86    @public
87    @type {Array}
88    @description Tasks to run before packaging
89   */
90  this.prereqs = prereqs;
91  /**
92    @name jake.PackageTask#packageDir
93    @public
94    @type {String='pkg'}
95    @description The directory-name to use for packaging the software
96   */
97  this.packageDir = 'pkg';
98  /**
99    @name jake.PackageTask#packageFiles
100    @public
101    @type {jake.FileList}
102    @description The list of files and directories to include in the
103    package-archive
104   */
105  this.packageFiles = new FileList();
106  /**
107    @name jake.PackageTask#needTar
108    @public
109    @type {Boolean=false}
110    @description If set to true, uses the `tar` utility to create
111    a gzip .tgz archive of the package
112   */
113  this.needTar = false;
114  /**
115    @name jake.PackageTask#needTarGz
116    @public
117    @type {Boolean=false}
118    @description If set to true, uses the `tar` utility to create
119    a gzip .tar.gz archive of the package
120   */
121  this.needTarGz = false;
122  /**
123    @name jake.PackageTask#needTarBz2
124    @public
125    @type {Boolean=false}
126    @description If set to true, uses the `tar` utility to create
127    a bzip2 .bz2 archive of the package
128   */
129  this.needTarBz2 = false;
130  /**
131    @name jake.PackageTask#needJar
132    @public
133    @type {Boolean=false}
134    @description If set to true, uses the `jar` utility to create
135    a .jar archive of the package
136   */
137  this.needJar = false;
138  /**
139    @name jake.PackageTask#needZip
140    @public
141    @type {Boolean=false}
142    @description If set to true, uses the `zip` utility to create
143    a .zip archive of the package
144   */
145  this.needZip = false;
146  /**
147    @name jake.PackageTask#manifestFile
148    @public
149    @type {String=null}
150    @description Can be set to point the `jar` utility at a manifest
151    file to use in a .jar archive. If unset, one will be automatically
152    created by the `jar` utility. This path should be relative to the
153    root of the package directory (this.packageDir above, likely 'pkg')
154   */
155  this.manifestFile = null;
156  /**
157    @name jake.PackageTask#tarCommand
158    @public
159    @type {String='tar'}
160    @description The shell-command to use for creating tar archives.
161   */
162  this.tarCommand = 'tar';
163  /**
164    @name jake.PackageTask#jarCommand
165    @public
166    @type {String='jar'}
167    @description The shell-command to use for creating jar archives.
168   */
169  this.jarCommand = 'jar';
170  /**
171    @name jake.PackageTask#zipCommand
172    @public
173    @type {String='zip'}
174    @description The shell-command to use for creating zip archives.
175   */
176  this.zipCommand = 'zip';
177  /**
178    @name jake.PackageTask#archiveNoBaseDir
179    @public
180    @type {Boolean=false}
181    @description Simple option for performing the archive on the
182    contents of the directory instead of the directory itself
183   */
184  this.archiveNoBaseDir = false;
185  /**
186    @name jake.PackageTask#archiveChangeDir
187    @public
188    @type {String=null}
189    @description Equivalent to the '-C' command for the `tar` and `jar`
190    commands. ("Change to this directory before adding files.")
191   */
192  this.archiveChangeDir = null;
193  /**
194    @name jake.PackageTask#archiveContentDir
195    @public
196    @type {String=null}
197    @description Specifies the files and directories to include in the
198    package-archive. If unset, this will default to the main package
199    directory -- i.e., name + version.
200   */
201  this.archiveContentDir = null;
202
203  if (typeof definition == 'function') {
204    definition.call(this);
205  }
206  this.define();
207};
208
209PackageTask.prototype = new (function () {
210
211  let _compressOpts = {
212    Tar: {
213      ext: '.tgz',
214      flags: 'czf',
215      cmd: 'tar'
216    },
217    TarGz: {
218      ext: '.tar.gz',
219      flags: 'czf',
220      cmd: 'tar'
221    },
222    TarBz2: {
223      ext: '.tar.bz2',
224      flags: 'cjf',
225      cmd: 'tar'
226    },
227    Jar: {
228      ext: '.jar',
229      flags: 'cf',
230      cmd: 'jar'
231    },
232    Zip: {
233      ext: '.zip',
234      flags: 'qr',
235      cmd: 'zip'
236    }
237  };
238
239  this.define = function () {
240    let self = this;
241    let packageDirPath = this.packageDirPath();
242    let compressTaskArr = [];
243
244    desc('Build the package for distribution');
245    task('package', self.prereqs.concat(['clobberPackage', 'buildPackage']));
246    // Backward-compat alias
247    task('repackage', ['package']);
248
249    task('clobberPackage', function () {
250      jake.rmRf(self.packageDir, {silent: true});
251    });
252
253    desc('Remove the package');
254    task('clobber', ['clobberPackage']);
255
256    let doCommand = function (p) {
257      let filename = path.resolve(self.packageDir + '/' + self.packageName() +
258                                  _compressOpts[p].ext);
259      if (process.platform == 'win32') {
260        // Windows full path may have drive letter, which is going to cause
261        // namespace problems, so strip it.
262        if (filename.length > 2 && filename[1] == ':') {
263          filename = filename.substr(2);
264        }
265      }
266      compressTaskArr.push(filename);
267
268      file(filename, [packageDirPath], function () {
269        let cmd;
270        let opts = _compressOpts[p];
271        // Directory to move to when doing the compression-task
272        // Changes in the case of zip for emulating -C option
273        let chdir = self.packageDir;
274        // Save the current dir so it's possible to pop back up
275        // after compressing
276        let currDir = process.cwd();
277        let archiveChangeDir;
278        let archiveContentDir;
279
280        if (self.archiveNoBaseDir) {
281          archiveChangeDir = self.packageName();
282          archiveContentDir = '.';
283        }
284        else {
285          archiveChangeDir = self.archiveChangeDir;
286          archiveContentDir = self.archiveContentDir;
287        }
288
289        cmd = self[opts.cmd + 'Command'];
290        cmd += ' -' + opts.flags;
291        if (opts.cmd == 'jar' && self.manifestFile) {
292          cmd += 'm';
293        }
294
295        // The name of the archive to create -- use full path
296        // so compression can be performed from a different dir
297        // if needed
298        cmd += ' ' + filename;
299
300        if (opts.cmd == 'jar' && self.manifestFile) {
301          cmd += ' ' + self.manifestFile;
302        }
303
304        // Where to perform the compression -- -C option isn't
305        // supported in zip, so actually do process.chdir for this
306        if (archiveChangeDir) {
307          if (opts.cmd == 'zip') {
308            chdir = path.join(chdir, archiveChangeDir);
309          }
310          else {
311            cmd += ' -C ' + archiveChangeDir;
312          }
313        }
314
315        // Where to get the archive content
316        if (archiveContentDir) {
317          cmd += ' ' + archiveContentDir;
318        }
319        else {
320          cmd += ' ' + self.packageName();
321        }
322
323        // Move into the desired dir (usually packageDir) to compress
324        // Return back up to the current dir after the exec
325        process.chdir(chdir);
326
327        exec(cmd, function (err, stdout, stderr) {
328          if (err) { throw err; }
329
330          // Return back up to the starting directory (see above,
331          // before exec)
332          process.chdir(currDir);
333
334          complete();
335        });
336      }, {async: true});
337    };
338
339    for (let p in _compressOpts) {
340      if (this['need' + p]) {
341        doCommand(p);
342      }
343    }
344
345    task('buildPackage', compressTaskArr, function () {});
346
347    directory(this.packageDir);
348
349    file(packageDirPath, this.packageFiles, function () {
350      jake.mkdirP(packageDirPath);
351      let fileList = [];
352      self.packageFiles.forEach(function (name) {
353        let f = path.join(self.packageDirPath(), name);
354        let fDir = path.dirname(f);
355        jake.mkdirP(fDir, {silent: true});
356
357        // Add both files and directories
358        fileList.push({
359          from: name,
360          to: f
361        });
362      });
363      let _copyFile = function () {
364        let file = fileList.pop();
365        let stat;
366        if (file) {
367          stat = fs.statSync(file.from);
368          // Target is a directory, just create it
369          if (stat.isDirectory()) {
370            jake.mkdirP(file.to, {silent: true});
371            _copyFile();
372          }
373          // Otherwise copy the file
374          else {
375            jake.cpR(file.from, file.to, {silent: true});
376            _copyFile();
377          }
378        }
379        else {
380          complete();
381        }
382      };
383      _copyFile();
384    }, {async: true});
385
386
387  };
388
389  this.packageName = function () {
390    if (this.version) {
391      return this.name + '-' + this.version;
392    }
393    else {
394      return this.name;
395    }
396  };
397
398  this.packageDirPath = function () {
399    return this.packageDir + '/' + this.packageName();
400  };
401
402})();
403
404jake.PackageTask = PackageTask;
405exports.PackageTask = PackageTask;
406
407