From b1e59c75b38c4a1d3ef974f4f121e239638cf4be Mon Sep 17 00:00:00 2001 From: Francisco Tolmasky Date: Wed, 23 Sep 2009 03:40:23 -0700 Subject: [PATCH] Initial commit. Reviewed by me. --- bin/jake | 5 + lib/jake.js | 658 +++++++++++++++++++++++++++++++++++ lib/jake/clean.js | 66 ++++ lib/jake/filecreationtask.js | 58 +++ lib/jake/filelist.js | 428 +++++++++++++++++++++++ lib/jake/filetask.js | 76 ++++ lib/jake/task.js | 366 +++++++++++++++++++ lib/jake/taskmanager.js | 292 ++++++++++++++++ package.json | 6 + 9 files changed, 1955 insertions(+) create mode 100755 bin/jake create mode 100644 lib/jake.js create mode 100644 lib/jake/clean.js create mode 100644 lib/jake/filecreationtask.js create mode 100644 lib/jake/filelist.js create mode 100644 lib/jake/filetask.js create mode 100644 lib/jake/task.js create mode 100644 lib/jake/taskmanager.js create mode 100644 package.json diff --git a/bin/jake b/bin/jake new file mode 100755 index 0000000..7734316 --- /dev/null +++ b/bin/jake @@ -0,0 +1,5 @@ +#!/usr/bin/env narwhal + +var jake = require("jake"); + +jake.application().run(); diff --git a/lib/jake.js b/lib/jake.js new file mode 100644 index 0000000..a79ec74 --- /dev/null +++ b/lib/jake.js @@ -0,0 +1,658 @@ +#!/usr/bin/env narwhal + +// Copyright 2009 280 North, Inc. (francisco@280north.com) +// Copyright 2003, 2004, 2005, 2006, 2007, 2008, 2009 by Jim Weirich (jim.weirich@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +var FILE = require("file"), + SYSTEM = require("system"), + + Task = require("jake/task").Task, + FileTask = require("jake/filetask").FileTask, + FileCreationTask = require("jake/filecreationtask").FileCreationTask, + TaskManager = require("jake/taskmanager").TaskManager; + +var DEFAULT_JAKEFILES = ["jakefile", "Jakefile", "jakefile.js", "Jakefile.js", "jakefile.j", "Jakefile.j"]; + +var Application = function() +{ + TaskManager.call(this); + + this._name = "rake"; + this._jakefiles = DEFAULT_JAKEFILES.slice(); + this._jakefile = null; + this._options = { }; +// this._pendingImports = []; +// this._imported = []; +// this.loaders = { }; +// this.defaultLoader = Rake::DefaultLoader.new + this._originalDirectory = FILE.cwd(); + this._topLevelTasks = []; +/* + add_loader('rb', DefaultLoader.new) + add_loader('rf', DefaultLoader.new) + add_loader('rake', DefaultLoader.new) + @tty_output = STDOUT.tty?*/ +} + +Application.__proto__ = TaskManager; +Application.prototype.__proto__ = TaskManager.prototype; + +// Run the Rake application. The run method performs the following three steps: +// +// * Initialize the command line options (+init+). +// * Define the tasks (+loadRakefile+). +// * Run the top level tasks (+runTasks+). +// +// If you wish to build a custom rake command, you should call +init+ on your +// application. The define any tasks. Finally, call +topLevel+ to run your top +// level tasks. +Application.prototype.run = function() +{ + this.init(); + this.loadRakefile(); + this.topLevel(); +} + +// Initialize the command line parameters and app name. +Application.prototype.init = function(/*String*/ anApplicationName) +{ + this._name = anApplicationName || "rake"; +// this.handleOptions(); +//FIXME: options + this.collectTasks(); +} + +// Find the rakefile and then load it and any pending imports. +Application.prototype.loadRakefile = function() +{ + this.rawLoadJakefile(); +} + +// Run the top level tasks of a Rake application. +Application.prototype.topLevel = function() +{/* + if (options.showTasks()) + this.displayTasksAndComments(); + + else if (options.showPrereqs) + this.displayPreq; + else +*/ + this._topLevelTasks.forEach(function(/*String*/ aTaskName) + { + this.invokeTask(aTaskName); + }, this); +} + +/* + # Add a loader to handle imported files ending in the extension + # +ext+. +Application.prototype. + + def add_loader(ext, loader) + ext = ".#{ext}" unless ext =~ /^\./ + @loaders[ext] = loader + end + + # Application options from the command line + def options + @options ||= OpenStruct.new + end + + # private ---------------------------------------------------------------- +*/ + +Application.prototype.invokeTask = function(/*String*/ aTaskString) +{ + var result = this.parseTaskString(aTaskString), + task = this.lookupTask(result[0]); + + task.invoke.apply(task, result[1]); +} + +Application.prototype.parseTaskString = function(/*String*/ aString) +{ + var matches = aString.match(/^([^\[]+)(\[(.*)\])$/); + + if (matches) + return [matches[0], matches[3].split(/\s*,\s*/)]; + + return [aString, []]; +} +/* + # Provide standard execption handling for the given block. + def standard_exception_handling + begin + yield + rescue SystemExit => ex + # Exit silently with current status + exit(ex.status) + rescue SystemExit, OptionParser::InvalidOption => ex + # Exit silently + exit(1) + rescue Exception => ex + # Exit with error message + $stderr.puts "#{name} aborted!" + $stderr.puts ex.message + if options.trace + $stderr.puts ex.backtrace.join("\n") + else + $stderr.puts ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || "" + $stderr.puts "(See full trace by running task with --trace)" + end + exit(1) + end + end +*/ + +// True if one of the files in RAKEFILES is in the current directory. +// If a match is found, it is copied into @rakefile. +Application.prototype.hasJakefile = function(/*String*/ aDirectory) +{ + var jakefiles = this._jakefiles, + index = 0, + count = jakefiles.length; + + for (; index < count; ++index) + { + var jakefile = jakefiles[index]; + + if (FILE.exists(FILE.join(aDirectory, jakefile))) + return jakefile; + + else if (jakefile === "") + return null; + } + + return null; +} +/* + # True if we are outputting to TTY, false otherwise + def tty_output? + @tty_output + end + + # Override the detected TTY output state (mostly for testing) + def tty_output=( tty_output_state ) + @tty_output = tty_output_state + end + + # We will truncate output if we are outputting to a TTY or if we've been + # given an explicit column width to honor + def truncate_output? + tty_output? || ENV['RAKE_COLUMNS'] + end + + # Display the tasks and comments. + def display_tasks_and_comments + displayable_tasks = tasks.select { |t| + t.comment && t.name =~ options.show_task_pattern + } + if options.full_description + displayable_tasks.each do |t| + puts "#{name} #{t.name_with_args}" + t.full_comment.split("\n").each do |line| + puts " #{line}" + end + puts + end + else + width = displayable_tasks.collect { |t| t.name_with_args.length }.max || 10 + max_column = truncate_output? ? terminal_width - name.size - width - 7 : nil + displayable_tasks.each do |t| + printf "#{name} %-#{width}s # %s\n", + t.name_with_args, max_column ? truncate(t.comment, max_column) : t.comment + end + end + end + + def terminal_width + if ENV['RAKE_COLUMNS'] + result = ENV['RAKE_COLUMNS'].to_i + else + result = unix? ? dynamic_width : 80 + end + (result < 10) ? 80 : result + rescue + 80 + end + + # Calculate the dynamic width of the + def dynamic_width + @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput) + end + + def dynamic_width_stty + %x{stty size 2>/dev/null}.split[1].to_i + end + + def dynamic_width_tput + %x{tput cols 2>/dev/null}.to_i + end + + def unix? + RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i + end + + def windows? + Win32.windows? + end + + def truncate(string, width) + if string.length <= width + string + else + ( string[0, width-3] || "" ) + "..." + end + end + + # Display the tasks and prerequisites + def display_prerequisites + tasks.each do |t| + puts "#{name} #{t.name}" + t.prerequisites.each { |pre| puts " #{pre}" } + end + end + + # A list of all the standard options used in rake, suitable for + # passing to OptionParser. + def standard_rake_options + [ + ['--classic-namespace', '-C', "Put Task and FileTask in the top level namespace", + lambda { |value| + require 'rake/classic_namespace' + options.classic_namespace = true + } + ], + ['--describe', '-D [PATTERN]', "Describe the tasks (matching optional PATTERN), then exit.", + lambda { |value| + options.show_tasks = true + options.full_description = true + options.show_task_pattern = Regexp.new(value || '') + } + ], + ['--dry-run', '-n', "Do a dry run without executing actions.", + lambda { |value| + verbose(true) + nowrite(true) + options.dryrun = true + options.trace = true + } + ], + ['--execute', '-e CODE', "Execute some Ruby code and exit.", + lambda { |value| + eval(value) + exit + } + ], + ['--execute-print', '-p CODE', "Execute some Ruby code, print the result, then exit.", + lambda { |value| + puts eval(value) + exit + } + ], + ['--execute-continue', '-E CODE', + "Execute some Ruby code, then continue with normal task processing.", + lambda { |value| eval(value) } + ], + ['--libdir', '-I LIBDIR', "Include LIBDIR in the search path for required modules.", + lambda { |value| $:.push(value) } + ], + ['--prereqs', '-P', "Display the tasks and dependencies, then exit.", + lambda { |value| options.show_prereqs = true } + ], + ['--quiet', '-q', "Do not log messages to standard output.", + lambda { |value| verbose(false) } + ], + ['--rakefile', '-f [FILE]', "Use FILE as the rakefile.", + lambda { |value| + value ||= '' + @rakefiles.clear + @rakefiles << value + } + ], + ['--rakelibdir', '--rakelib', '-R RAKELIBDIR', + "Auto-import any .rake files in RAKELIBDIR. (default is 'rakelib')", + lambda { |value| options.rakelib = value.split(':') } + ], + ['--require', '-r MODULE', "Require MODULE before executing rakefile.", + lambda { |value| + begin + require value + rescue LoadError => ex + begin + rake_require value + rescue LoadError => ex2 + raise ex + end + end + } + ], + ['--rules', "Trace the rules resolution.", + lambda { |value| options.trace_rules = true } + ], + ['--no-search', '--nosearch', '-N', "Do not search parent directories for the Rakefile.", + lambda { |value| options.nosearch = true } + ], + ['--silent', '-s', "Like --quiet, but also suppresses the 'in directory' announcement.", + lambda { |value| + verbose(false) + options.silent = true + } + ], + ['--system', '-g', + "Using system wide (global) rakefiles (usually '~/.rake/*.rake').", + lambda { |value| options.load_system = true } + ], + ['--no-system', '--nosystem', '-G', + "Use standard project Rakefile search paths, ignore system wide rakefiles.", + lambda { |value| options.ignore_system = true } + ], + ['--tasks', '-T [PATTERN]', "Display the tasks (matching optional PATTERN) with descriptions, then exit.", + lambda { |value| + options.show_tasks = true + options.show_task_pattern = Regexp.new(value || '') + options.full_description = false + } + ], + ['--trace', '-t', "Turn on invoke/execute tracing, enable full backtrace.", + lambda { |value| + options.trace = true + verbose(true) + } + ], + ['--verbose', '-v', "Log message to standard output.", + lambda { |value| verbose(true) } + ], + ['--version', '-V', "Display the program version.", + lambda { |value| + puts "rake, version #{RAKEVERSION}" + exit + } + ] + ] + end + + # Read and handle the command line options. + def handle_options + options.rakelib = ['rakelib'] + + OptionParser.new do |opts| + opts.banner = "rake [-f rakefile] {options} targets..." + opts.separator "" + opts.separator "Options are ..." + + opts.on_tail("-h", "--help", "-H", "Display this help message.") do + puts opts + exit + end + + standard_rake_options.each { |args| opts.on(*args) } + end.parse! + + # If class namespaces are requested, set the global options + # according to the values in the options structure. + if options.classic_namespace + $show_tasks = options.show_tasks + $show_prereqs = options.show_prereqs + $trace = options.trace + $dryrun = options.dryrun + $silent = options.silent + end + end + + # Similar to the regular Ruby +require+ command, but will check + # for *.rake files in addition to *.rb files. + def rake_require(file_name, paths=$LOAD_PATH, loaded=$") + return false if loaded.include?(file_name) + paths.each do |path| + fn = file_name + ".rake" + full_path = File.join(path, fn) + if File.exist?(full_path) + load full_path + loaded << fn + return true + end + end + fail LoadError, "Can't find #{file_name}" + end +*/ + +Application.prototype.findJakefileLocation = function() +{ + var directory = FILE.cwd(), + filename = null; + + while (!(filename = this.hasJakefile(directory)))// && !this._options.nosearch) + directory = FILE.join(directory, ".."); + + if (!filename) + return null; + + return [filename, directory]; +} + +Application.prototype.rawLoadJakefile = function() +{ + var result = this.findJakefileLocation(), + jakefile = result ? result[0] : null, + location = result ? result[1] : null, + options = this._options; +/* + if (!options.ignore_system && (options.load_system || !rakefile) && system_dir && FILE.directory?(system_dir) + if (options["silent"]) + print("(in "); + puts "(in #{Dir.pwd})" unless options.silent + glob("#{system_dir}/*.rake") do |name| + add_import name + end + } + else*/ + { + if (!jakefile) + throw "No Jakefile found (looking for: " + this._rakefiles.join(', ') + ")"; + + this._jakefile = jakefile; + +// file.chdir(location); + + if (!options["silent"]) + print("(in " + FILE.cwd() + ")"); + +// $rakefile = @rakefile if options.classic_namespace + + if (jakefile && jakefile.length)//expand_path? + require(FILE.absolute(FILE.join(location, jakefile))); + +/* options.rakelib.each do |rlib| + glob("#{rlib}/*.rake") do |name| + add_import name + end + */ + } + //load_imports +} + +/* + def glob(path, &block) + Dir[path.gsub("\\", '/')].each(&block) + end + private :glob +*/ +/* + # The directory path containing the system wide rakefiles. + def system_dir + @system_dir ||= + begin + if ENV['RAKE_SYSTEM'] + ENV['RAKE_SYSTEM'] + elsif Win32.windows? + Win32.win32_system_dir + else + standard_system_dir + end + end + end + + # The standard directory containing system wide rake files. + def standard_system_dir #:nodoc: + File.join(File.expand_path('~'), '.rake') + end + private :standard_system_dir +*/ + +// Collect the list of tasks on the command line. If no tasks are +// given, return a list containing only the default task. +// Environmental assignments are processed at this time as well. +Application.prototype.collectTasks = function() +{ + this._topLevelTasks = []; + + var topLevelTasks = this._topLevelTasks; + + SYSTEM.args.slice(1).forEach(function(/*String*/ anArgument) + { + var matches = anArgument.match(/^(\w+)=(.*)$/); + + if (matches) + SYSTEM.env[matches[1]] = matches[2]; + + else if (!anArgument.match(/^-/)) + topLevelTasks.push(anArgument); + }); + + if (topLevelTasks.length <= 0) + topLevelTasks.push("default"); +} +/* + # Add a file to the list of files to be imported. + def add_import(fn) + @pending_imports << fn + end + + # Load the pending list of imported files. + def load_imports + while fn = @pending_imports.shift + next if @imported.member?(fn) + if fn_task = lookup(fn) + fn_task.invoke + end + ext = File.extname(fn) + loader = @loaders[ext] || @default_loader + loader.load(fn) + @imported << fn + end + end + + # Warn about deprecated use of top level constant names. + def const_warning(const_name) + @const_warning ||= false + if ! @const_warning + $stderr.puts %{WARNING: Deprecated reference to top-level constant '#{const_name}' } + + %{found at: #{rakefile_location}} # ' + $stderr.puts %{ Use --classic-namespace on rake command} + $stderr.puts %{ or 'require "rake/classic_namespace"' in Rakefile} + end + @const_warning = true + end + + def rakefile_location + begin + fail + rescue RuntimeError => ex + ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || "" + end + end + */ + +// Exports +exports.Task = Task; +exports.FileTask = FileTask; +exports.FileCreationTask = FileCreationTask; +exports.TaskManager = TaskManager; +exports.Application = Application; + +var application = null; + +exports.application = function() +{ + if (!application) + application = new Application(); + + return application; +} + +exports.setApplication = function(/*Application*/ anApplication) +{ + application = anApplication; +} + +exports.EARLY = new Date(-10000,1,1,0,0,0,0).getTime(); + +exports.task = function() +{ + return Task.defineTask.apply(Task, arguments); +} + +exports.file = function() +{ + return FileTask.defineTask.apply(FileTask, arguments); +} + +exports.fileCreate = function() +{ + return FileCreationTask.defineTask.apply(FileCreationTask, arguments); +} + +exports.directory = function(aDirectory) +{ + var oldLength = null; + + while (aDirectory !== "." && aDirectory.length !== oldLength) + { + exports.fileCreate(aDirectory, function(aTask) + { + var taskName = aTask.name(); + + if (!FILE.exists(taskName)) + FILE.mkdirs(taskName); + }); + + oldLength = aDirectory.length; + aDirectory = FILE.dirname(aDirectory); + } +} + +exports.filedir = function() +{ + var fileTask = FileTask.defineTask.apply(FileTask, arguments), + fileDirectory = FILE.dirname(fileTask.name()); + + exports.directory (fileDirectory); + exports.file (fileTask.name(), fileDirectory); +} +/* + # Return the original directory where the Rake application was started. + def original_dir + application.original_dir + end +*/ diff --git a/lib/jake/clean.js b/lib/jake/clean.js new file mode 100644 index 0000000..ce68820 --- /dev/null +++ b/lib/jake/clean.js @@ -0,0 +1,66 @@ +#!/usr/bin/env narwhal + +// Copyright 2009 280 North, Inc. (francisco@280north.com) +// Copyright 2003, 2004, 2005, 2006, 2007, 2008, 2009 by Jim Weirich (jim.weirich@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + + +// The 'jake/clean' file defines two file lists (CLEAN and CLOBBER) and +// two jake tasks ("clean" and "clobber"). +// +// ["clean"] Clean up the project by deleting scratch files and backup +// files. Add files to the CLEAN file list to have the :clean +// target handle them. +// +// ["clobber"] Clobber all generated and non-source files in a project. +// The task depends on :clean, so all the clean files will +// be deleted as well as files in the CLOBBER file list. +// The intent of this task is to return a project to its +// pristine, just unpacked state. + +var Jake = require("jake"); + +CLEAN = [];//new Jake.FileList("**/*~", "**/*.bak", "**/core"); +/* +CLEAN.clearExclude().exclude(function(aFilename) +{ + aFilename.pathmap("%f") == 'core' && File.directory?(fn) +});*/ + +//desc "Remove any temporary products." +task ("clean", function() +{ + CLEAN.forEach(function(aFilename) + { + FILE.rmTree(aFilename); + }); +}); + +CLOBBER = new Jake::FileList; + +//desc "Remove any generated file." +task ("clobber", ["clean"], function() +{ + CLOBBER.forEach(function(aFilename) + { + FILE.rmTree(aFilename); + }); +}); diff --git a/lib/jake/filecreationtask.js b/lib/jake/filecreationtask.js new file mode 100644 index 0000000..dba66a8 --- /dev/null +++ b/lib/jake/filecreationtask.js @@ -0,0 +1,58 @@ +#!/usr/bin/env narwhal + +// Copyright 2009 280 North, Inc. (francisco@280north.com) +// Copyright 2003, 2004, 2005, 2006, 2007, 2008, 2009 by Jim Weirich (jim.weirich@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +var FILE = require("file"), + Jake = require("jake"), + FileTask = require("jake/filetask").FileTask; + +// ######################################################################### +// A FileCreationTask is a file task that when used as a dependency will be +// needed if and only if the file has not been created. Once created, it is +// not re-triggered if any of its dependencies are newer, nor does trigger +// any rebuilds of tasks that depend on it whenever it is updated. +// +var FileCreationTask = function() +{ + FileTask.apply(this, arguments); +} + +FileCreationTask.__proto__ = FileTask; +FileCreationTask.prototype.__proto__ = FileTask.prototype; + +//print("IT IS " + FileTask.defineTask + " " + FileCreationTask.defineTask); +// Is this file task needed? Yes if it doesn't exist. +FileCreationTask.prototype.isNeeded = function() +{ + return !FILE.exists(this.name()); +} + +// Time stamp for file creation task. This time stamp is earlier +// than any other time stamp. +FileCreationTask.prototype.timestamp = function() +{ + return Jake.EARLY; +} + +// Exports +exports.FileCreationTask = FileCreationTask; diff --git a/lib/jake/filelist.js b/lib/jake/filelist.js new file mode 100644 index 0000000..af4c047 --- /dev/null +++ b/lib/jake/filelist.js @@ -0,0 +1,428 @@ +#!/usr/bin/env narwhal + +// Copyright 2009 280 North, Inc. (francisco@280north.com) +// Copyright 2003, 2004, 2005, 2006, 2007, 2008, 2009 by Jim Weirich (jim.weirich@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + + +// A FileList is essentially an array with a few helper methods defined to +// make file manipulation a bit easier. +// +// FileLists are lazy. When given a list of glob patterns for possible files +// to be included in the file list, instead of searching the file structures +// to find the files, a FileList holds the pattern for latter use. +// +// This allows us to define a number of FileList to match any number of +// files, but only search out the actual files when then FileList itself is +// actually used. The key is that the first time an element of the +// FileList/Array is requested, the pending patterns are resolved into a real +// list of file names. +// + +function isRegExp(anObject) +{ + return typeof(anObject) === "function") && anObject.constructor.toString().match(/regexp/i) !== null +} + +// Create a file list from the globbable patterns given. +// +// Example: +// file_list = new FileList('lib/**/*.rb', 'test/test*.rb') +// +function FileList() +{ + this._pending_add = [] + this._pending = false + this._excludePatterns = DEFAULT_IGNORE_PATTERNS.slice(); + this._excludeProcs = DEFAULT_IGNORE_PROCS.slice(); + this._excludeRe = nil + this._items = [] + this.include.apply(this, arguments); +} + # == Method Delegation + # + # The lazy evaluation magic of FileLists happens by implementing all the + # array specific methods to call +resolve+ before delegating the heavy + # lifting to an embedded array object (@items). + # + # In addition, there are two kinds of delegation calls. The regular kind + # delegates to the @items array and returns the result directly. Well, + # almost directly. It checks if the returned value is the @items object + # itself, and if so will return the FileList object instead. + # + # The second kind of delegation call is used in methods that normally + # return a new Array object. We want to capture the return value of these + # methods and wrap them in a new FileList object. We enumerate these + # methods in the +SPECIAL_RETURN+ list below. + + # List of array methods (that are not in +Object+) that need to be + # delegated. + ARRAY_METHODS = (Array.instance_methods - Object.instance_methods).map { |n| n.to_s } + + # List of additional methods that must be delegated. + MUST_DEFINE = %w[to_a inspect] + + # List of methods that should not be delegated here (we define special + # versions of them explicitly below). + MUST_NOT_DEFINE = %w[to_a to_ary partition *] + + # List of delegated methods that return new array values which need + # wrapping. + SPECIAL_RETURN = %w[ + map collect sort sort_by select find_all reject grep + compact flatten uniq values_at + + - & | + ] + + DELEGATING_METHODS = (ARRAY_METHODS + MUST_DEFINE - MUST_NOT_DEFINE).collect{ |s| s.to_s }.sort.uniq + + # Now do the delegation. + DELEGATING_METHODS.each_with_index do |sym, i| + if SPECIAL_RETURN.include?(sym) + ln = __LINE__+1 + class_eval %{ + def #{sym}(*args, &block) + resolve + result = @items.send(:#{sym}, *args, &block) + FileList.new.import(result) + end + }, __FILE__, ln + else + ln = __LINE__+1 + class_eval %{ + def #{sym}(*args, &block) + resolve + result = @items.send(:#{sym}, *args, &block) + result.object_id == @items.object_id ? self : result + end + }, __FILE__, ln + end + end + + + +// Add file names defined by glob patterns to the file list. If an array +// is given, add each element of the array. +// +// Example: +// file_list.include("*.java", "*.cfg") +// file_list.include %w( math.c lib.h *.o ) +// +FileList.prototype.include = function() +{ + // TODO: check for pending + Array.prototype.forEach.apply(arguments, function(/*Object*/ anArgument) + { + if (Array.isArray(anArgument)) + this.include.apply(this, anArgument); + else if (typeof anArgument.toArray === "function") + this.include.apply(this, anArgument.toArray()); + else + this._pendingAdd.push(aFilename); + }, this); + + this._pending = true; + + return this; +} + +FileList.prototype.add = FileList.prototype.include; + +// Register a list of file name patterns that should be excluded from the +// list. Patterns may be regular expressions, glob patterns or regular +// strings. In addition, a block given to exclude will remove entries that +// return true when given to the block. +// +// Note that glob patterns are expanded against the file system. If a file +// is explicitly added to a file list, but does not exist in the file +// system, then an glob pattern in the exclude list will not exclude the +// file. +// +// Examples: +// FileList['a.c', 'b.c'].exclude("a.c") => ['b.c'] +// FileList['a.c', 'b.c'].exclude(/^a/) => ['b.c'] +// +// If "a.c" is a file, then ... +// FileList['a.c', 'b.c'].exclude("a.*") => ['b.c'] +// +// If "a.c" is not a file, then ... +// FileList['a.c', 'b.c'].exclude("a.*") => ['a.c', 'b.c'] +// +FileList.prototype.exclude = function() +{ + Array.prototype.forEach.apply(arguments, function(argument) + { + if (typeof argument === "function") + this._exlcudeProcs.push(argument); + else + this._excludePatterns.push(argument); + }, this); + + if (!this._pending) + this.resolveExclude(); + + return this; +} + +FileList.prototype.clearExclude = function() +{ + this._excludePatterns = []; + this._excludeProcs = []; + + if (!this._pending) + this.calculateExcludeRegexp(); + + return this; +} + +FileList.prototype.toArray = function() +{ + this.resolve(); + return this._items; +} +/* + # Lie about our class. + def is_a?(klass) + klass == Array || super(klass) + end + alias kind_of? is_a? + + # Redefine * to return either a string or a new file list. + def *(other) + result = @items * other + case result + when Array + FileList.new.import(result) + else + result + end + end +*/ + +// Resolve all the pending adds now. +FileList.prototype.resolve = function() +{ + if (this._pending) + { + this._pending = false; + + this._pendingAdd.forEach(function(/*String*/ aFileName) + { + this.resolveAdd(aFilename); + }, this); + + this._pendingAdd = []; + + this.resolveExclude(); + } + + return this; +} + +FileList.prototype.calculateExcludeRegexp = function() +{ + var ignores = []; + + this._excludePatterns.forEach(function(aPattern) + { + if (isRegExp(aPattern)) + ignores.push(aPattern) + /* + else if () + + case pat + when Regexp + ignores << pat + when /[*?]/ + Dir[pat].each do |p| ignores << p end + else + ignores << Regexp.quote(pat) + end*/ + end + if ignores.empty? + @exclude_re = /^$/ + else + re_str = ignores.collect { |p| "(" + p.to_s + ")" }.join("|") + @exclude_re = Regexp.new(re_str) + end + end +} + +FileList.prototype.resolveAdd = function(/*String*/ aFilename) +{ + + case fn + when %r{[*?\[\{]} + add_matching(fn) + else + self << fn + end + end + private :resolve_add + + def resolve_exclude + calculate_exclude_regexp + reject! { |fn| exclude?(fn) } + self + end + private :resolve_exclude +} + + # Return a new FileList with the results of running +sub+ against each + # element of the oringal list. + # + # Example: + # FileList['a.c', 'b.c'].sub(/\.c$/, '.o') => ['a.o', 'b.o'] + # + def sub(pat, rep) + inject(FileList.new) { |res, fn| res << fn.sub(pat,rep) } + end + + # Return a new FileList with the results of running +gsub+ against each + # element of the original list. + # + # Example: + # FileList['lib/test/file', 'x/y'].gsub(/\//, "\\") + # => ['lib\\test\\file', 'x\\y'] + # + def gsub(pat, rep) + inject(FileList.new) { |res, fn| res << fn.gsub(pat,rep) } + end + + # Same as +sub+ except that the oringal file list is modified. + def sub!(pat, rep) + each_with_index { |fn, i| self[i] = fn.sub(pat,rep) } + self + end + + # Same as +gsub+ except that the original file list is modified. + def gsub!(pat, rep) + each_with_index { |fn, i| self[i] = fn.gsub(pat,rep) } + self + end + + # Apply the pathmap spec to each of the included file names, returning a + # new file list with the modified paths. (See String#pathmap for + # details.) + def pathmap(spec=nil) + collect { |fn| fn.pathmap(spec) } + end + + # Return a new file list with String#ext method applied + # to each member of the array. + # + # This method is a shortcut for: + # + # array.collect { |item| item.ext(newext) } + # + # +ext+ is a user added method for the Array class. + def ext(newext='') + collect { |fn| fn.ext(newext) } + end + + + # Grep each of the files in the filelist using the given pattern. If a + # block is given, call the block on each matching line, passing the file + # name, line number, and the matching line of text. If no block is given, + # a standard emac style file:linenumber:line message will be printed to + # standard out. + def egrep(pattern) + each do |fn| + open(fn) do |inf| + count = 0 + inf.each do |line| + count += 1 + if pattern.match(line) + if block_given? + yield fn, count, line + else + puts "#{fn}:#{count}:#{line}" + end + end + end + end + end + end + + # Return a new file list that only contains file names from the current + # file list that exist on the file system. + def existing + select { |fn| File.exist?(fn) } + end + + # Modify the current file list so that it contains only file name that + # exist on the file system. + def existing! + resolve + @items = @items.select { |fn| File.exist?(fn) } + self + end + + # FileList version of partition. Needed because the nested arrays should + # be FileLists in this version. + def partition(&block) # :nodoc: + resolve + result = @items.partition(&block) + [ + FileList.new.import(result[0]), + FileList.new.import(result[1]), + ] + end + + # Convert a FileList to a string by joining all elements with a space. + def to_s + resolve + self.join(' ') + end + + # Add matching glob patterns. + def add_matching(pattern) + Dir[pattern].each do |fn| + self << fn unless exclude?(fn) + end + end + private :add_matching + + # Should the given file name be excluded? + def exclude?(fn) + calculate_exclude_regexp unless @exclude_re + fn =~ @exclude_re || @exclude_procs.any? { |p| p.call(fn) } + end + +var DEFAULT_IGNORE_PATTERNS = + [ + /(^|[\/\\])CVS([\/\\]|$)/, + /(^|[\/\\])\.svn([\/\\]|$)/, + /\.bak$/, + /~$/ + ]; + + DEFAULT_IGNORE_PROCS = + [ + proc { |fn| fn =~ /(^|[\/\\])core$/ && ! File.directory?(fn) } + ] +# @exclude_patterns = DEFAULT_IGNORE_PATTERNS.dup + +FileList.prototype.import = function(/*Array*/ anArray) +{ + this._items = array + return this; +} diff --git a/lib/jake/filetask.js b/lib/jake/filetask.js new file mode 100644 index 0000000..0091641 --- /dev/null +++ b/lib/jake/filetask.js @@ -0,0 +1,76 @@ +#!/usr/bin/env narwhal + +// Copyright 2009 280 North, Inc. (francisco@280north.com) +// Copyright 2003, 2004, 2005, 2006, 2007, 2008, 2009 by Jim Weirich (jim.weirich@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +var FILE = require("file"), + Jake = require("jake"), + Task = require("jake/task").Task; + +// A FileTask is a task that includes time based dependencies. If any of a +// FileTask's prerequisites have a timestamp that is later than the file +// represented by this task, then the file must be rebuilt (using the +// supplied actions). +var FileTask = function() +{ + Task.apply(this, arguments); +} + +FileTask.__proto__ = Task; +FileTask.prototype.__proto__ = Task.prototype; + +// Is this file task needed? Yes if it doesn't exist, or if its time stamp +// is out of date. +FileTask.prototype.isNeeded = function() +{ + return !FILE.exists(this.name()) || this.outOfDate(this.timestamp()); +} + +// Time stamp for file task. +FileTask.prototype.timestamp = function() +{ + if (FILE.exists(this.name())) + return FILE.mtime(this.name()); + + return Jake.EARLY; +} + +// Are there any prerequisites with a later time than the given time stamp? +FileTask.prototype.outOfDate = function(aTimestamp) +{ + var application = this.application(); + + this._prerequisites.some(function(aTaskName) + { + return application.lookupTask(aTaskName).timestamp() > aTimestamp; + }, this); +} + +// Apply the scope to the task name according to the rules for this kind +// of task. File based tasks ignore the scope when creating the name. +FileTask.scopeName = function(/*Array*/ aScope, /*String*/ aTaskName) +{ + return aTaskName; +} + +// EXPORTS +exports.FileTask = FileTask; diff --git a/lib/jake/task.js b/lib/jake/task.js new file mode 100644 index 0000000..743447d --- /dev/null +++ b/lib/jake/task.js @@ -0,0 +1,366 @@ +#!/usr/bin/env narwhal + +// Copyright 2009 280 North, Inc. (francisco@280north.com) +// Copyright 2003, 2004, 2005, 2006, 2007, 2008, 2009 by Jim Weirich (jim.weirich@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +var SYSTEM = require("system"), + jake = require("jake"); + +var Task = function(/*String*/ aName, /*Application*/ anApplication) +{ + this._name = aName; + this._prerequisites = []; + this._actions = []; + this._alreadyInvoked = false; + //this._fullComment = null; + //this._comment = null; + //this.lock = Monitor.new + this._application = anApplication; + this._scope = anApplication.currentScope(); + this._argumentNames = []; +} + +Task.prototype.toString = function() +{ + return "Task (" + this.name() + ")"; +} + +Task.prototype.name = function() +{ + return this._name; +} + +Task.prototype.prerequisites = function() +{ + return this._prerequisites; +} + +Task.prototype.actions = function() +{ + return this._actions; +} + +Task.prototype.application = function() +{ + return this._application; +} + +Task.prototype.enhance = function(/*Array*/ prerequisites, /*Function*/ anAction) +{ + if (prerequisites) + this._prerequisites = this._prerequisites.concat(prerequisites); + + if (anAction) + this._actions.push(anAction); +} + +Task.prototype.reenable = function() +{ + this._alreadyInvoked = false; +} + +Task.prototype.clear = function() +{ + this.clearPrerequisites(); + this.clearActions(); +} + +Task.prototype.clearPrerequisites = function() +{ + this._prerequisites = []; +} + +Task.prototype.clearActions = function() +{ + this._actions = []; +} + +Task.prototype.invoke = function() +{ + var taskArguments = new TaskArguments(this._argumentNames, Array.prototype.slice.apply(arguments)); + + this.invokeWithCallChain(taskArguments, InvocationChain.EMPTY); +} + +// Same as invoke, but explicitly pass a call chain to detect +// circular dependencies. +Task.prototype.invokeWithCallChain = function(taskArguments, anInvocationChain) +{ + var newChain = InvocationChain.append(this, anInvocationChain); +// @lock.synchronize do +// if application.options.trace +// puts "** Invoke #{name} #{format_trace_flags}" +// end + + if (this._alreadyInvoked) + return; + + this._alreadyInvoked = true; + + this.invokePrerequisites(taskArguments, newChain); + + if (this.isNeeded()) + this.execute(taskArguments); +} + +// Invoke all the prerequisites of a task. +Task.prototype.invokePrerequisites = function(taskArguments, invocationChain) +{ + this._prerequisites.forEach(function(/*String*/ aPrerequisiteName) + { + var prerequisite = this._application.lookupTask(aPrerequisiteName, this._scope); + + prerequisiteArguments = taskArguments.newScope(prerequisite.argumentNames()); + prerequisite.invokeWithCallChain(prerequisiteArguments, invocationChain); + }, this); +} + +Task.prototype.setArgumentNames = function(argumentNames) +{ + this._argumentNames = argumentNames; +} + +Task.prototype.argumentNames = function() +{ + return this._argumentNames; +} + +// Execute the actions associated with this task. +Task.prototype.execute = function(taskArguments) +{ + taskArguments = taskArguments || EMPTY_TASK_ARGS; + +// if application.options.dryrun +// puts "** Execute (dry run) #{name}" +// return +// end +// if application.options.trace +// puts "** Execute #{name}" +// end + +// application.enhance_with_matching_rule(name) if @actions.empty? + + this._actions.forEach(function(anAction) + { + anAction(this); + //anAction(This, args) + }, this); +} + +Task.prototype.isNeeded = function() +{ + return true; +} + +Task.prototype.timestamp = function() +{ + if (this._prerequisites.length <= 0) + return new Date().getTime(); + + return Math.max.apply(null, this._prerequisites.map(function(/*String*/ aPrerequisiteName) + { + return this._application.lookupTask(aPrerequisiteName).timestamp(); + }, this)); +} + +Task.clear = function() +{ + Jake.application.clear(); +} + +Task.tasks = function() +{ + return Jake.application.tasks(); +} + +Task.taskNamed = function(/*String*/ aTaskName) +{ + return Jake.application.taskWithName(aTaskName); +} + +Task.taskWithNameIsDefined = function(/*String*/ aTaskName) +{ + return !!Jake.application.lookupTaskWithName(aTaskName); +} + +Task.defineTask = function() +{ + var args = [this]; + application = jake.application(); + + // Can't simply use concat because we don't want to flatten inner arrays. + Array.prototype.forEach.call(arguments, function(object) + { + args.push(object); + }); + + return application.defineTask.apply(application, args); +} + +Task.scopeName = function(/*Array*/ aScope, /*String*/ aTaskName) +{ + return aScope.concat(aTaskName).join(':'); +} +/* + # Track the last comment made in the Rakefile. + attr_accessor :last_description + alias :last_comment :last_description # Backwards compatibility +*/ + +var TaskArguments = function(/*Array*/ names, /*Array*/ values, /*TaskArguments*/ aParent) +{ + this._names = names.slice(); + this._parent = aParent; + this._hash = { }; + + this._names.forEach(function(/*String*/ aName, /*Number*/ anIndex) + { + if (values[anIndex] !== undefined) + this._hash[aName] = values[anIndex]; + }, this); +} + +TaskArguments.prototype.newScope = function(/*Array*/ names) +{ + var values = names.map(function(/*String*/ aName) + { + return this.lookup(aName); + }, this); + + return new TaskArguments(names, values, this); +} + +TaskArguments.prototype.withDefaults = function(/*Object*/ defaults) +{ + var hash = this._hash; + + for (key in defaults) + if (defaults.hasOwnProperty(key) && !hash.hasOwnProperty(key)) + hash[key] = defaults[key]; +} + +TaskArguments.prototype.forEach = function(/*Function*/ aFunction, /*Object*/ thisObject) +{ + if (!aFunction) + return; + + var hash = this._hash; + + if (typeof thisObject === "undefined") + thisObject = aFunction; + + for (key in hash) + aFunction.apply(thisObject, [key, hash[key]]); +} + +TaskArguments.prototype.toHash = function() +{ + return this._hash; +} + +TaskArguments.prototype.toString = function() +{ + return this._hash; +} + +TaskArguments.prototype.lookup = function(/*String*/ aName) +{ + var hash = this._hash; + + if (hash.hasOwnProperty(aName)) + return hash[Name]; + + var env = SYSTEM.env; + + if (env.hasOwnProperty(aName)) + return env[aName]; + + var upperCaseName = aName.toUpperCase(); + + if (env.hasOwnProperty(upperCaseName)) + return env[upperCaseName]; + + if (this._parent) + return this._parent.lookup(aName); + + return null; +} + +var EMPTY_TASK_ARGS = new TaskArguments([], []); + +var InvocationChain = function(aValue, /*InvocationChain*/ aTail) +{ + this._value = aValue; + this._tail = aTail; +} + +InvocationChain.prototype.isMember = function(/*Object*/ anObject) +{ + return this._value == anObject || this._tail.isMember(anObject); +} + +InvocationChain.prototype.append = function(/*Object*/ anObject) +{ + if (this.isMember(anObject)) + throw "Circular dependency detected: " + this + " => " + this._value; + + return new InvocationChain(this._value, this); +} + +InvocationChain.prototype.toString = function() +{ + return this.prefix() + this._value; +} + +InvocationChain.append = function(aValue, /*InvocationChain*/ aChain) +{ + return aChain.append(aValue); +} + +InvocationChain.prototype.prefix = function() +{ + return this._tail + " => "; +} + +var EmptyInvocationChain = function() +{ +} + +EmptyInvocationChain.prototype.isMember = function(/*Object*/ anObject) +{ + return false; +} + +EmptyInvocationChain.prototype.append = function(/*Object*/ aValue) +{ + return new InvocationChain(aValue, this); +} + +EmptyInvocationChain.prototype.toString = function() +{ + return "TOP"; +} + +InvocationChain.EMPTY = new EmptyInvocationChain; + +// EXPORTS +exports.Task = Task; diff --git a/lib/jake/taskmanager.js b/lib/jake/taskmanager.js new file mode 100644 index 0000000..4e27033 --- /dev/null +++ b/lib/jake/taskmanager.js @@ -0,0 +1,292 @@ +#!/usr/bin/env narwhal + +// Copyright 2009 280 North, Inc. (francisco@280north.com) +// Copyright 2003, 2004, 2005, 2006, 2007, 2008, 2009 by Jim Weirich (jim.weirich@gmail.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +var FILE = require("file"), + FileTask = require("jake/filetask").FileTask; + +TaskManager = function() +{ + this._tasks = { }; + this._rules = []; + this._scope = []; +// @last_description = nil +} +/* + def create_rule(*args, &block) + pattern, arg_names, deps = resolve_args(args) + pattern = Regexp.new(Regexp.quote(pattern) + '$') if String === pattern + @rules << [pattern, deps, block] + end +*/ + +TaskManager.prototype.defineTask = function(aTaskClass, aTaskName) +{ + if (arguments.length < 1) + throw "No class passed to Task.defineTask"; + + if (arguments.length < 2) + throw "No name passed to Task.defineTask"; + + aTaskName = aTaskClass.scopeName(this._scope, aTaskName); + + var task = this.intern(aTaskClass, aTaskName), + result = this.resolveArguments(Array.prototype.slice.apply(arguments, [2])); + + task.setArgumentNames(result[0]); +//task.add_description(@last_description) +//@last_description = nil + task.enhance(result[1], result[2]); + + return task; +} + +// Lookup a task. Return an existing task if found, otherwise +// create a task of the current type. +TaskManager.prototype.intern = function(/*Function*/ aTaskClass, /*String*/ aTaskName) +{ + var task = this._tasks[aTaskName]; + + if (!task) + { + task = new aTaskClass(aTaskName, this); + this._tasks[aTaskName] = task; + } + + return task; +} + +TaskManager.prototype.lookupTask = function(/*String*/ aTaskName, /*Array*/ scopes) +{ + var task = this.lookup(aTaskName, scopes) || /* enhance_with_matching_rule(task_name) or ||*/ this.synthesizeFileTask(aTaskName); + + if (!task) + throw "Don't know how to build task '" + aTaskName + "'"; + + return task; +} + +TaskManager.prototype.synthesizeFileTask = function(/*String*/ aTaskName) +{ + if (!FILE.exists(aTaskName)) + return null; + + return this.defineTask(FileTask, aTaskName); +} + +// Resolve the arguments for a task/rule. Returns a triplet of +// [task_name, arg_name_list, prerequisites]. +// +// The patterns recognized by this argument resolving function are: +// +// task(taskName, action) +// task(taskName, [dependency]) +// task(taskName, [dependency], action) +// task(taskName, [argumentName], [dependency], action) +// +TaskManager.prototype.resolveArguments = function(args) +{ + var action = null; + + if (args.length && (typeof args[args.length - 1] === "function")) + action = args.pop(); + + var dependencies = []; + + if (args.length) + dependencies = args.pop(); + + var argumentNames = []; + + if (args.length) + argumentNames = args.pop(); + + return [argumentNames, dependencies, action]; +} + +TaskManager.prototype.tasks = function() +{ + var tasks = Object.keys(this._tasks); + + tasks.sort(function(lhs, rhs) + { + if (lhs < rhs) + return -1; + + else if (lhs > rhs) + return 1; + + return 0; + } ); + + return tasks; +} + +// List of all the tasks defined in the given scope (and its +// sub-scopes). +TaskManager.prototype.tasksInScope = function(/*Array*/ aScope) +{ + var prefix = aScope.join(":"), + regexp = new Regexp("^" + prefix + ":"); + + return this._tasks.filter(function(/*Task*/ aTask) + { + return !!aTask.name().match(regexp); + }); +} + +// Clear all tasks in this application. +TaskManager.prototype.clear = function() +{ + this._tasks = []; + this._rules = []; +} + +// Lookup a task, using scope and the scope hints in the task name. +// This method performs straight lookups without trying to +// synthesize file tasks or rules. Special scope names (e.g. '^') +// are recognized. If no scope argument is supplied, use the +// current scope. Return nil if the task cannot be found. +TaskManager.prototype.lookup = function(/*String*/ aTaskName, /*Array*/ initialScope) +{ + if (!initialScope) + initialScope = this._scope; + + var scopes = initialScope, + matches = null; + + if (aTaskName.match(/^jake:/)) + { + scopes = []; + aTaskName = aTaskName.replace(/^jake:/, ""); + } + else if (matches = aTaskName.match(/^(\^+)/)) + { + scopes = initialScope.slice(0, initialScope.length - matches[1].length); + aTaskName = aTaskName.replace(/^(\^+)/, ""); + } + + return this.lookupInScope(aTaskName, scopes); +} + +// Lookup the task name +TaskManager.prototype.lookupInScope = function(/*String*/ aTaskName, /*Array*/ aScope) +{ + var count = aScope.length; + + while (count >= 0) + { + var task = this._tasks[aScope.slice(0, count).concat([aTaskName]).join(':')]; + + if (task) + return task; + + count--; + } + + return null; +} + +// Return the list of scope names currently active in the task +// manager. +TaskManager.prototype.currentScope = function() +{ + return this._scope.slice(); +} +/* + # Evaluate the block in a nested namespace named +name+. Create + # an anonymous namespace if +name+ is nil. + def in_namespace(name) + name ||= generate_name + @scope.push(name) + ns = NameSpace.new(self, @scope) + yield(ns) + ns + ensure + @scope.pop + end + + private + + # Generate an anonymous namespace name. + def generate_name + @seed ||= 0 + @seed += 1 + "_anon_#{@seed}" + end + + def trace_rule(level, message) + puts "#{" "*level}#{message}" if Rake.application.options.trace_rules + end + + # Attempt to create a rule given the list of prerequisites. + def attempt_rule(task_name, extensions, block, level) + sources = make_sources(task_name, extensions) + prereqs = sources.collect { |source| + trace_rule level, "Attempting Rule #{task_name} => #{source}" + if File.exist?(source) || Rake::Task.task_defined?(source) + trace_rule level, "(#{task_name} => #{source} ... EXIST)" + source + elsif parent = enhance_with_matching_rule(source, level+1) + trace_rule level, "(#{task_name} => #{source} ... ENHANCE)" + parent.name + else + trace_rule level, "(#{task_name} => #{source} ... FAIL)" + return nil + end + } + task = FileTask.define_task({task_name => prereqs}, &block) + task.sources = prereqs + task + end + + # Make a list of sources from the list of file name extensions / + # translation procs. + def make_sources(task_name, extensions) + extensions.collect { |ext| + case ext + when /%/ + task_name.pathmap(ext) + when %r{/} + ext + when /^\./ + task_name.ext(ext) + when String + ext + when Proc + if ext.arity == 1 + ext.call(task_name) + else + ext.call + end + else + fail "Don't know how to handle rule dependent: #{ext.inspect}" + end + }.flatten + end + + end # TaskManager +*/ + +// EXPORTS +exports.TaskManager = TaskManager; diff --git a/package.json b/package.json new file mode 100644 index 0000000..29924ea --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "name": "jake", + "author": "Francisco Tolmasky (http://tolmasky.com/)", + "description": "A build system for CommonJS, lifted from Rake", + "keywords": ["build", "jake", "rake", "make"] +} \ No newline at end of file