text:  'use strict';
  
  var build       = exports;
  var fs          = require('fs');
  var fse         = require('fs-extra');
  var expand      = require('fs-expand');
  var node_path   = require('path');
  var semver      = require('semver-extra');
  var semver_helper = require('../util/semver');
  var async       = require('async');
  var cortex_json   = require('read-cortex-json');
  var ln          = require('../util/link');
  var run_scripts = require('../util/run-scripts');
  var makeArray   = require('make-array');
  var mix         = require('mix2');
  var builder     = require('neuron-builder');
  var ngraph      = require('neuron-graph');
  var neuron      = require('neuronjs');
  var util        = require('util');
  var MD5         = require('MD5');
  
  // @param {Object} options
  //      see ./lib/option/build.js for details
  build.run = function(options, callback) {
    this.MESSAGE = this.locale.require('command-build');
    options.install = options['install-build'];
    options.prerelease = options.prerelease || this.profile.get('prerelease');
  
    var self = this;
    var tasks = options.install
      ? [
        'simplely_read_cortex_json',
        'read_cortex_config_js',
        'prepare_build_type',
        'run_preinstall_script',
        'run_prebuild_script',
        'build_process',
        'server_link'
      ] : [
        'read_cortex_json',
        'clean_cortex_json',
        'read_cortex_config_js',
        'prepare_build_type',
        'run_preinstall_script',
        'run_prebuild_script',
        // #478
        // `cortex.main` and many other files might have not been generated before `cortex.scripts`
        // so, we clean cortex_json
        'build_process',
        'server_link',
        'run_postbuild_script',
        'run_postwatch_script'
      ];
    async.eachSeries(tasks, function (task, done) {
      self[task](options, done);
    }, callback);
  };
  
  build.read_cortex_config_js = function(options, callback){
    // If install from cache, cortex.json will be enhanced json.
    var file = node_path.join(options.cwd, 'cortex.config.js');
    fs.exists(file, function(exists){
      options.build_config = exists ? require(file) : {};
      callback(null);
    });
  }
  
  build.prepare_build_type = function(options, callback){
    var pkg = options.pkg;
    var entries = pkg.entries;
    var csses = pkg.css;
    var file = options.file;
  
    function hasFile(list){
      return list.map(function(rel){
        return node_path.join(options.cwd, rel);
      }).indexOf(file) > -1;
    }
  
    function isMod() {
      return /\.js$/.test(file);
    }
  
    if(hasFile(entries)){
      options.build_type = 'js';
    }else if(hasFile(csses)){
      options.build_type = 'css';
    }else if(build._in_directories(options, file)){
      options.build_type = 'dir';
    }else if(isMod()) {
      options.build_type = 'mod';
    }else{
      options.build_type = 'full';
    }
  
    callback();
  };
  
  build._in_directories = function(options, file) {
    if(!file){return false;}
  
    var directories = options.pkg.directories || {};
    var dir, path;
    for (dir in directories) {
      if(dir == 'dist'){continue;}
  
      path = node_path.join(options.cwd, directories[dir]);
      if (file.indexOf(path) == 0) {return true};
    }
  
    return false;
  };
  
  build.simplely_read_cortex_json = function (options, callback) {
    // If install from cache, cortex.json will be enhanced json.
    var file = node_path.join(options.cwd, 'cortex.json');
    var self = this;
    fse.readJson(file, function (err, json) {
      if (err) {
        return cb(err);
      }
  
      function cb (err, json) {
        if (err) {
          if (err.message) {
            err.message += ' File: "' + file + '"';
          }
          return callback(err);
        }
        options.pkg = json;
        self.add_prerease(options, json);
        callback(null);
      }
  
      // Legacy
      // Before cortexjs/read-cortex-json#11,
      // the `main`, `css` and `entries` are not always existing.
      if (!('main' in json) || !util.isArray(json.css) || !util.isArray(json.entries)) {
        return cortex_json.clean(options.cwd, json, cb);
      }
  
      cb(null, json);
    }.bind(this));
  };
  
  
  build.read_cortex_json = function(options, callback) {
    var self = this;
    cortex_json.extra(options.cwd, function (err, pkg) {
      if (err) {
        return callback(err);
      }
      options.pkg = pkg;
      self.add_prerease(options, pkg);
      callback(null);
    });
  };
  
  
  build.add_prerease = function (options, pkg) {
    var pr = options.prerelease;
    if (pr) {
      var version = pkg.version;
      pkg.version = semver_helper.add_prerelease(version, pr);
      if (!semver.isStable(version) && !semver.isPrerelease(version)) {
        this.logger.warn(
            'Package "' + pkg.name + '@' + version + '" is already a prerelease version, '
          + 'but will be built as "' + pkg.version + '" according to option or config.'
        );
      }
    }
  };
  
  
  build.clean_cortex_json = function (options, callback) {
    cortex_json.clean(options.cwd, options.pkg, callback);
  };
  
  
  // Run custom build scripts, such as 'grunt'
  build.run_preinstall_script = function(options, callback) {
    if (!options.preinstall) {
      return callback(null);
    }
    this.run_script('preinstall', options, callback);
  };
  
  
  // Run custom build scripts, such as 'grunt'
  build.run_prebuild_script = function(options, callback) {
    // #436: if build when install, it should not run scripts.prebuild
    if (!options.prebuild) {
      return callback(null);
    }
  
    if(options.build_type != 'full'){
      return callback(null);
    }
  
    this.run_script('prebuild', options, callback);
  };
  
  build.run_postbuild_script = function(options, callback){
    if (!options.prebuild) {
      return callback(null);
    }
    this.run_script('postbuild', options, callback);
  }
  
  // Hook for watcher first build
  // you can do stuffs like opening browser here
  build.run_postwatch_script = function(options, callback){
    if (!options.prebuild || !options.init) {
      return callback(null);
    }
  
    this.run_script('postwatch', options, callback);
  }
  
  
  build.run_script = function(script, options, callback) {
    var pkg = options.pkg;
    var scripts =
      makeArray(pkg.scripts && pkg.scripts[script])
      // skip empty scripts
      .filter(Boolean);
  
    if (!scripts.length) {
      return callback(null);
    }
  
    var self = this;
  
    this.logger.info('{{cyan run}} "scripts.' + script + '" ...');
  
    run_scripts(scripts, options).on('spawn', function(command) {
      self.logger.info(' - {{cyan exec}} "' + command + '" ...');
  
    }).on('close', function(code, signal) {
      if (code) {
        callback({
          code: 'EBUILDSCRIPT',
          message: 'build step "scripts.' + script + '" executes as a failure. exit code: ' + code,
          data: {
            code: code,
            signal: signal,
            script: script
          }
        });
      } else {
        callback(null);
      }
  
    }).on('error', function(err) {
      self.logger.warn('"scripts.' + script + '" let out an error: ' + err);
    });
  };
  
  
  build.build_process = function(options, callback) {
    var pkg = options.pkg;
    var to = node_path.join(options.dest, pkg.name, pkg.version);
    options.to = to;
  
    var basic_tasks = [
      'write_cortex_json',
      'copy_shrinkwrap_json',
      'build_engine'
    ];
  
    // distribution directory
    var dist = options.pkg.directories && options.pkg.directories.dist;
    if (dist) {
      options.dist = dist;
      basic_tasks.push(
        'check_dist'
      );
    } else {
      basic_tasks.push(
        'copy_csses',
        'copy_directories',
        'build_modules',
        'write_md5_json',
        'generate_md5_files',
        'generate_config'
      );
    }
  
    var self = this;
  
    if(options.build_type == 'full'){
      basic_tasks.unshift('clean_dest');
    }
  
    async.eachSeries(basic_tasks, function (task, done) {
      self[task](options, done);
    }, callback);
  };
  
  
  // We should clean the dest folder before we
  build.clean_dest = function (options, callback) {
    fs.exists(options.to, function (exists) {
      if (!exists) {
        return callback(null);
      }
  
      // #477
      // If we delete some files,
      // these files such as "cortex-shrinkwrap.json" and "src/*" should also be removed
      // from the dest folder.
      fse.remove(options.to, callback);
    });
  };
  
  
  build.is_facade_package = function(pkg){
    var name = pkg.name;
    return name.indexOf("app-") == 0 || pkg.type == "app";
  };
  
  build.traverse_file_md5 = function(options, callback){
    var to = options.dir;
    var ignore = options.ignore;
    var fullMD5 = options.fullMD5;
    var result = {};
    function readdir(dir, callback){
      fs.readdir(dir, function(err, list){
        async.map(list, function(item, done){
          if(item.match(ignore)){
            return done(null);
          }
          var fullpath = node_path.join(dir, item);
  
          fs.stat(fullpath, function(err, stat){
            if(err){
              return done(err);
            }
            if(stat.isDirectory()){
              readdir(fullpath, done);
            }else{
              fs.readFile(fullpath, function(err, content){
                if(err){
                  return done(err);
                }
                var md5 = MD5(content);
                if(!fullMD5){
                  md5 = md5.slice(0,8);
                }
                result[node_path.relative(to, fullpath)] = md5;
                done(null);
              });
            }
          });
        }, function(err){
          callback(err);
        });
      });
    }
  
    readdir(to, function(err){
      callback(err, result);
    });
  }
  
  build.write_md5_json = function(options, callback){
    if(!build.is_facade_package(options.pkg)){
      return callback(null);
    }
    var to = options.to;
    var md5_file = node_path.join(options.to, 'md5.json');
    var logger = this.logger;
  
    build.traverse_file_md5({
      dir: to,
      ignore: "cortex.json",
      fullMD5: !!options["full-md5"]
    }, function(err, md5){
      if(err){
        return callback(err);
      }
  
      logger.info('{{cyan write}} ' + md5_file);
      fse.outputJson(md5_file, md5, callback);
    });
  }
  
  build.generate_md5_files = function(options, callback){
    if(!build.is_facade_package(options.pkg)){
      return callback(null);
    }
    if(!options["generate-md5"]){
      return callback(null);
    }
  
    var logger = this.logger;
    var to = options.to;
    var md5_file = node_path.join(options.to, 'md5.json');
    function appendMD5(path, md5){
      return
    }
    fse.readJson(md5_file, function(err, json){
      if(err){
        return callback(err);
      }
  
      var path_objs = Object.keys(json).map(function(path){
        return {
          full_path: node_path.join(to, path),
          path: path
        }
      });
      async.map(path_objs, function(path_obj, done){
        var md5 = json[path_obj.path];
        var ext = node_path.extname(path_obj.full_path);
        var md5_path = path_obj.full_path.split(ext)[0] + "_" + md5 + ext;
        logger.info("{{cyan copy}} " + md5_path);
        fse.copy(path_obj.full_path, md5_path, done)
      }, callback);
    });
  }
  
  build.write_cortex_json = function (options, callback) {
    var cortex_file = node_path.join(options.to, 'cortex.json');
    fse.outputJson(cortex_file, options.pkg, callback);
  };
  
  
  build.copy_shrinkwrap_json = function (options, callback) {
    this.copy(options.cwd, options.to, 'cortex-shrinkwrap.json', callback);
  };
  
  
  build.server_link = function (options, callback) {
    if (options.install) {
      return callback(null);
    }
  
    if(options.build_type != 'full'){
      return callback(null);
    }
  
    var built_root = this.profile.get('built_root');
    if (built_root === options.dest) {
      return callback(null);
    }
  
    var pkg_dir = node_path.join(options.pkg.name, options.pkg.version);
    var from = node_path.join(built_root, pkg_dir);
    var to = node_path.join(options.dest, pkg_dir);
    var logger = this.logger;
    ln.link(from, to, function (err) {
      if (err) {
        return callback(err);
      }
  
      logger.info('{{cyan link}} ' + from + ' -> ' + to);
      callback(null);
    });
  };
  
  
  build.check_dist = function (options, callback) {
    var rel_dist = options.dist;
    var dist = node_path.join(options.cwd, rel_dist);
    // if `dist` dir exists, skip building and just copy it
    fs.exists(dist, function(exists) {
      if (!exists) {
        // if `cortex.directories.dist` is declared, the dir must be existed.
        return callback({
          code: 'DIST_NOT_FOUND',
          message: 'dist dir "' + dist + '" does not exist.',
          data: {
            dist: dist
          }
        });
      }
  
      this.logger.info('dist dir "' + dist + '" found, {{cyan skip}} building ...');
      this.copy_dist(rel_dist, options, callback);
    }.bind(this));
  };
  
  
  // copy dist dir to the destination dirs
  build.copy_dist = function(dist, options, callback) {
    var self = this;
    var tasks = options.tasks;
    var dist_dir = node_path.join(options.cwd, dist);
    self.copy(dist_dir, options.to, null, callback);
  };
  
  
  // Builds JavaScript modules
  build.build_modules = function(options, callback) {
    var pkg = options.pkg;
    var build_type = options.build_type;
  
    if(!this._is_type(build_type, 'js') && !this._is_type(build_type, 'mod')) {
      return callback(null);
    }
  
    var loaders = options.build_config.loaders || [];
  
    // `pkg.entries` must be an array
    var entries = [].concat(pkg.entries);
    if (pkg.main && !~entries.indexOf(pkg.main)) {
      entries.push(pkg.main);
    }
  
    if (!entries.length) {
      // Pure css package.
      return callback(null);
    }
  
    var cwd = options.cwd;
    var to = options.to;
    var self = this;
  
    if(build_type == 'js'){
      entries = [node_path.relative(options.cwd, options.file)];
    }
  
    async.eachSeries(entries, function (entry, done) {
      var from = node_path.join(cwd, entry);
      builder({
        cwd: cwd,
        targetVersion: pkg.version,
        pkg: options.pkg,
        changedFile: options.file,
        loaders: loaders
      })
      .on('warn', function (warn) {
        self.logger.warn(warn.message || warn);
      })
      .parse(from, function (err, content) {
        if (err) {
          if (err.code == 'ENOTNEEDBUILD') {
            self.logger.debug('Not need build:', entry);
            return done(null);
          }
          return done(err);
        }
  
        var file_to = entry === pkg.main
          // It is a convention that main entry will built to <name>.js
          ? pkg.name + '.js'
          : entry;
  
        if (file_to != file_to.toLowerCase()) {
          self.logger.warn(util.format('"%s" contains uppercase characters', file_to));
          file_to = file_to.toLowerCase();
        }
  
        var path_to = node_path.join(to, file_to);
        fse.outputFile(path_to, content, function (err) {
          if (err) {
            return done(err);
          }
  
          self.logger.info('{{cyan write}} ' + path_to);
          done(null);
        });
      });
    }, callback);
  };
  
  build._is_type = function(build_type, type){
    // build_type is from step prepare filelist
    return build_type == 'full' || build_type == type;
  };
  
  build.copy_csses = function (options, callback) {
    var css = options.pkg.css;
    var build_type = options.build_type;
    if (!css) {
      return callback(null);
    }
  
    if(!this._is_type(build_type, 'css')){
      return callback(null);
    }
  
    var self = this;
    var to = options.to;
  
    if(build_type == 'css'){
      css = [node_path.relative(options.cwd, options.file)];
    }
  
    async.eachSeries(css, function (path, done) {
      var from = options.cwd;
      var css_path = node_path.join(from, path);
  
      self.copy(from, to, path, function (err) {
        if (err && err.code === 'SRC_NOT_FOUND') {
          err = {
            code: 'CSS_NOT_FOUND',
            message: '`pkg.css`, "' + path + '" is declared but not found.',
            data: {
              path: path
            }
          };
        }
        if(err){
          return done(err);
        }
        var csspath = node_path.resolve(from, path);
  
        fs.stat(csspath, function(err, stat){
          if(err){return done(err);}
  
          if(stat.isFile()){
            self.parse_css_images(csspath, function(err, image_paths){
              if(err){return done(err);}
  
              async.eachSeries(image_paths, function(image_path, done){
                var css_dir = node_path.dirname(csspath);
                var full_image_path = node_path.join(css_dir, image_path);
                // avoid to copying these images twice
                if (self._in_directories(options, full_image_path)) {
                  done(null);
                  return;
                }
  
                self.copy(from, to, node_path.relative(from, full_image_path), done);
              }, done);
            });
          }else{
            done(null);
          }
        });
  
  
      }, true);
    }, callback);
  };
  
  build.parse_css_images = function(csspath, done){
    var self = this;
  
    var image_path_cache = build.image_path_cache = build.image_path_cache || {};
  
    fs.readFile(csspath, function(err, content){
      if(err){return done(err);}
      /**
       * match
       * 1. url(a.png)
       * 2. url('http://i1.static.dp/s/c/i/b.png')
       * 3. url("./c.png")
       */
      var reg = /url\(\s*(['"]?)([^"'\)]*)\1\s*\)/g;
      var m;
      var imgpath;
      var image_paths = [];
  
      function isRelative(imgpath) {
        return !/^https?:\/\//.test(imgpath);
      }
  
      function isDataURI(imgpath){
        return imgpath.indexOf("data:") == 0;
      }
  
      while(m = reg.exec(content)){
        imgpath = m[0].match(/url\(\s*(['"]?)([^"'\)]*)\1\s*\)/)[2];
        if(isRelative(imgpath) && !isDataURI(imgpath) && !image_path_cache[imgpath]){
          image_paths.push(imgpath);
          image_path_cache[imgpath] = true;
        }
      }
  
      done(null,image_paths);
    });
  
  
  }
  
  
  build.copy_directories = function(options, callback) {
    var self = this;
    var pkg = options.pkg;
    var cwd = options.cwd;
    var directories = pkg.directories || {};
    var to = options.to;
    var file = options.file;
    var build_type = options.build_type;
  
    if(!this._is_type(build_type, 'dir')){
      return callback(null);
    }
  
    if (build_type == 'dir' && file) {
      // only copy the changed file
      file = node_path.relative(cwd, file);
      self.copy(cwd, to, file, function(err) {
        callback(err);
      });
      return;
    }
  
    var tasks = [
      // We only support `directories.src` for now.
      'src',
      'html',
      'img',
      'font'
    ].filter(function (dir) {
      return directories[dir];
    });
  
  
    async.eachSeries(tasks, function (name, done) {
      var dir = directories[name];
      self.copy(cwd, to, dir, function (err) {
        if (err && err.code === 'SRC_NOT_FOUND') {
          err = {
            code: 'DIR_NOT_FOUND',
            message: '`directories.' + name + '.' + dir + '` is defined in cortex.json, but not found.',
            data: {
              name: name,
              dir: dir
            }
          };
        }
        done(err);
      }, true);
  
    }, callback);
  };
  
  
  // Copy item from `from` to `to`
  // @param {String=} item If is undefined, will copy `from` to `to`
  // @param {Boolean} strict
  build.copy = function(from, to, item, callback, strict) {
    var self = this;
    if (from === to) {
      callback(null);
      return;
    }
  
    if (item) {
      if (item != item.toLowerCase()) {
        self.logger.warn(util.format('"%s" contains uppercase characters', item));
      }
  
      item = item.toLowerCase();
      from = node_path.join(from, item);
      to = node_path.join(to, item);
    }
  
    fs.exists(from, function (exists) {
      if (!exists) {
        // if strict and the source is not found, an error will throw.
        if (strict) {
          return callback({
            code: 'SRC_NOT_FOUND'
          });
        } else {
          return callback(null);
        }
      }
  
      self.logger.info('{{cyan copy}} ' + from + ' -> ' + to);
      var err;
      try{
        fse.copySync(from, to);
      }catch(e){
        err = e;
      }
      callback(err);
    });
  };
  
  
  build.build_engine = function (options, callback) {
    if (options.install) {
      return callback(null);
    }
    var dest = node_path.join(options.dest, 'neuron', neuron.version(), 'neuron.js');
    var neuron_js = node_path.join(options.dest, 'neuron.js');
  
    // Chrome on Windows could not open a symlink of a javascript file,
    // so we just write both of the files, and use no symlink.
    async.each([dest, neuron_js], function (to, done) {
      neuron.write(to, done, true);
    }, callback);
  };
  
  
  build.generate_config = function (options, callback) {
    if (options.install || !options.config) {
      return callback(null);
    }
  
    var pkg = options.pkg;
    var config = {};
    async.series([
      function (done){
        ngraph(pkg, {
          built_root: node_path.join(options.cwd, 'neurons'),
          cwd: options.cwd
        }, function(err, graph, shrinkwrap){
          done(err, graph);
        });
      },
      function (done){
        var md5_file = node_path.join(options.to, 'md5.json');
        if(build.is_facade_package(options.pkg)){
          fse.readJson(md5_file, done);
        }else{
          done(null);
        }
      }
    ], function(err, results){
      if(err){
        return callback(err);
      }
      var graph = results[0];
      var md5 = results[1];
      var graph_entry = graph._;
      graph_entry[pkg.name + '@*'] = graph_entry[pkg.name + '@' + pkg.version];
  
      var config_file = node_path.join(options.dest, 'config.js');
      if(graph){config.graph = graph;}
      if(md5 && options["generate-md5"]){
        config.hash = {};
        config.hash[pkg.name + "@" + pkg.version] = md5;
      }
      fse.outputFile(config_file, 'neuron.config(' + JSON.stringify(config, null, 2) + ');', callback);
    });
  };
  