1let path = require('path'); 2let fs = require('fs'); 3let Task = require('./task/task').Task; 4 5// Split a task to two parts, name space and task name. 6// For example, given 'foo:bin/a%.c', return an object with 7// - 'ns' : foo 8// - 'name' : bin/a%.c 9function splitNs(task) { 10 let parts = task.split(':'); 11 let name = parts.pop(); 12 let ns = resolveNs(parts); 13 return { 14 'name' : name, 15 'ns' : ns 16 }; 17} 18 19// Return the namespace based on an array of names. 20// For example, given ['foo', 'baz' ], return the namespace 21// 22// default -> foo -> baz 23// 24// where default is the global root namespace 25// and -> means child namespace. 26function resolveNs(parts) { 27 let ns = jake.defaultNamespace; 28 for(let i = 0, l = parts.length; ns && i < l; i++) { 29 ns = ns.childNamespaces[parts[i]]; 30 } 31 return ns; 32} 33 34// Given a pattern p, say 'foo:bin/a%.c' 35// Return an object with 36// - 'ns' : foo 37// - 'dir' : bin 38// - 'prefix' : a 39// - 'suffix' : .c 40function resolve(p) { 41 let task = splitNs(p); 42 let name = task.name; 43 let ns = task.ns; 44 let split = path.basename(name).split('%'); 45 return { 46 ns: ns, 47 dir: path.dirname(name), 48 prefix: split[0], 49 suffix: split[1] 50 }; 51} 52 53// Test whether string a is a suffix of string b 54function stringEndWith(a, b) { 55 let l; 56 return (l = b.lastIndexOf(a)) == -1 ? false : l + a.length == b.length; 57} 58 59// Replace the suffix a of the string s with b. 60// Note that, it is assumed a is a suffix of s. 61function stringReplaceSuffix(s, a, b) { 62 return s.slice(0, s.lastIndexOf(a)) + b; 63} 64 65class Rule { 66 constructor(opts) { 67 this.pattern = opts.pattern; 68 this.source = opts.source; 69 this.prereqs = opts.prereqs; 70 this.action = opts.action; 71 this.opts = opts.opts; 72 this.desc = opts.desc; 73 this.ns = opts.ns; 74 } 75 76 // Create a file task based on this rule for the specified 77 // task-name 78 // ====== 79 // FIXME: Right now this just throws away any passed-in args 80 // for the synthsized task (taskArgs param) 81 // ====== 82 createTask(fullName, level) { 83 let self = this; 84 let pattern; 85 let source; 86 let action; 87 let opts; 88 let prereqs; 89 let valid; 90 let src; 91 let tNs; 92 let createdTask; 93 let name = Task.getBaseTaskName(fullName); 94 let nsPath = Task.getBaseNamespacePath(fullName); 95 let ns = this.ns.resolveNamespace(nsPath); 96 97 pattern = this.pattern; 98 source = this.source; 99 100 if (typeof source == 'string') { 101 src = Rule.getSource(name, pattern, source); 102 } 103 else { 104 src = source(name); 105 } 106 107 // TODO: Write a utility function that appends a 108 // taskname to a namespace path 109 src = nsPath.split(':').filter(function (item) { 110 return !!item; 111 }).concat(src).join(':'); 112 113 // Generate the prerequisite for the matching task. 114 // It is the original prerequisites plus the prerequisite 115 // representing source file, i.e., 116 // 117 // rule( '%.o', '%.c', ['some.h'] ... 118 // 119 // If the objective is main.o, then new task should be 120 // 121 // file( 'main.o', ['main.c', 'some.h' ] ... 122 prereqs = this.prereqs.slice(); // Get a copy to work with 123 prereqs.unshift(src); 124 125 // Prereq should be: 126 // 1. an existing task 127 // 2. an existing file on disk 128 // 3. a valid rule (i.e., not at too deep a level) 129 valid = prereqs.some(function (p) { 130 let ns = self.ns; 131 return ns.resolveTask(p) || 132 fs.existsSync(Task.getBaseTaskName(p)) || 133 jake.attemptRule(p, ns, level + 1); 134 }); 135 136 // If any of the prereqs aren't valid, the rule isn't valid 137 if (!valid) { 138 return null; 139 } 140 // Otherwise, hunky-dory, finish creating the task for the rule 141 else { 142 // Create the action for the task 143 action = function () { 144 let task = this; 145 self.action.apply(task); 146 }; 147 148 opts = this.opts; 149 150 // Insert the file task into Jake 151 // 152 // Since createTask function stores the task as a child task 153 // of currentNamespace. Here we temporariliy switch the namespace. 154 // FIXME: Should allow optional ns passed in instead of this hack 155 tNs = jake.currentNamespace; 156 jake.currentNamespace = ns; 157 createdTask = jake.createTask('file', name, prereqs, action, opts); 158 createdTask.source = src.split(':').pop(); 159 jake.currentNamespace = tNs; 160 161 return createdTask; 162 } 163 } 164 165 match(name) { 166 return Rule.match(this.pattern, name); 167 } 168 169 // Test wether the a prerequisite matchs the pattern. 170 // The arg 'pattern' does not have namespace as prefix. 171 // For example, the following tests are true 172 // 173 // pattern | name 174 // bin/%.o | bin/main.o 175 // bin/%.o | foo:bin/main.o 176 // 177 // The following tests are false (trivally) 178 // 179 // pattern | name 180 // bin/%.o | foobin/main.o 181 // bin/%.o | bin/main.oo 182 static match(pattern, name) { 183 let p; 184 let task; 185 let obj; 186 let filename; 187 188 if (pattern instanceof RegExp) { 189 return pattern.test(name); 190 } 191 else if (pattern.indexOf('%') == -1) { 192 // No Pattern. No Folder. No Namespace. 193 // A Simple Suffix Rule. Just test suffix 194 return stringEndWith(pattern, name); 195 } 196 else { 197 // Resolve the dir, prefix and suffix of pattern 198 p = resolve(pattern); 199 200 // Resolve the namespace and task-name 201 task = splitNs(name); 202 name = task.name; 203 204 // Set the objective as the task-name 205 obj = name; 206 207 // Namespace is already matched. 208 209 // Check dir 210 if (path.dirname(obj) != p.dir) { 211 return false; 212 } 213 214 filename = path.basename(obj); 215 216 // Check file name length 217 if ((p.prefix.length + p.suffix.length + 1) > filename.length) { 218 // Length does not match. 219 return false; 220 } 221 222 // Check prefix 223 if (filename.indexOf(p.prefix) !== 0) { 224 return false; 225 } 226 227 // Check suffix 228 if (!stringEndWith(p.suffix, filename)) { 229 return false; 230 } 231 232 // OK. Find a match. 233 return true; 234 } 235 } 236 237 // Generate the source based on 238 // - name name for the synthesized task 239 // - pattern pattern for the objective 240 // - source pattern for the source 241 // 242 // Return the source with properties 243 // - dep the prerequisite of source 244 // (with the namespace) 245 // 246 // - file the file name of source 247 // (without the namespace) 248 // 249 // For example, given 250 // 251 // - name foo:bin/main.o 252 // - pattern bin/%.o 253 // - source src/%.c 254 // 255 // return 'foo:src/main.c', 256 // 257 static getSource(name, pattern, source) { 258 let dep; 259 let pat; 260 let match; 261 let file; 262 let src; 263 264 // Regex pattern -- use to look up the extension 265 if (pattern instanceof RegExp) { 266 match = pattern.exec(name); 267 if (match) { 268 if (typeof source == 'function') { 269 src = source(name); 270 } 271 else { 272 src = stringReplaceSuffix(name, match[0], source); 273 } 274 } 275 } 276 // Assume string 277 else { 278 // Simple string suffix replacement 279 if (pattern.indexOf('%') == -1) { 280 if (typeof source == 'function') { 281 src = source(name); 282 } 283 else { 284 src = stringReplaceSuffix(name, pattern, source); 285 } 286 } 287 // Percent-based substitution 288 else { 289 pat = pattern.replace('%', '(.*?)'); 290 pat = new RegExp(pat); 291 match = pat.exec(name); 292 if (match) { 293 if (typeof source == 'function') { 294 src = source(name); 295 } 296 else { 297 file = match[1]; 298 file = source.replace('%', file); 299 dep = match[0]; 300 src = name.replace(dep, file); 301 } 302 } 303 } 304 } 305 306 return src; 307 } 308} 309 310 311exports.Rule = Rule; 312