From 38052804869a75e421b67895793e2b90fedd5f88 Mon Sep 17 00:00:00 2001 From: Dominic Monroe Date: Fri, 19 May 2017 17:12:11 +0100 Subject: [PATCH] Add support for dynamically matching targets There is a basis protocol (Target) which exposes match? for whether or not a target matches. A sequential search is then performed over all targets for each input target. Example usage of this feature: ``` ;; Machfile.edn {#mach/glob "*.md" {product "somefile" produce (str "# A title")}} $ mach hello.md Writing somefile ``` Further functionality that is necessary to make this feature useful is the exposure of the match ctx (already assoced onto the target when it matches) so that code can refer to the full path. To extend the example above, the future feature should allow the product to be set based on the matching input. I haven't thoroughly tested this, so a second pair of eyes over this would be good. --- package.json | 11 ++-- src/mach/core.cljs | 136 ++++++++++++++++++++++++++++++--------------- 2 files changed, 97 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index feb30fe..ed05783 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,11 @@ }, "homepage": "https://github.com/juxt/mach#README.md", "dependencies": { - "ini": "^1.3.4", - "lumo-cljs": "1.4.1", - "toposort": "^1.0.0", - "tmp": "0.0.31", - "yargs": "^8.0.1" + "ini": "^1.3.4", + "lumo-cljs": "1.4.1", + "micromatch": "^2.3.11", + "tmp": "0.0.31", + "toposort": "^1.0.0", + "yargs": "^8.0.1" } } diff --git a/src/mach/core.cljs b/src/mach/core.cljs index f01d83a..a928996 100755 --- a/src/mach/core.cljs +++ b/src/mach/core.cljs @@ -11,6 +11,7 @@ [lumo.repl :as repl] [lumo.classpath] [clojure.walk :refer [postwalk]] + [clojure.set :refer [map-invert]] [clojure.string :as str])) (defonce ^:private st (cljs/empty-state)) @@ -21,18 +22,68 @@ (def path (nodejs/require "path")) (def temp (nodejs/require "tmp")) (def yargs (nodejs/require "yargs")) +(def mm (nodejs/require "micromatch")) -(defn target-order [machfile target-name] - (map symbol - (drop 1 ; drop nil - (js->clj - (toposort - (clj->js - (tree-seq - (fn [[_ target-name]] (-> machfile (get target-name) (get 'depends))) - (fn [[_ target-name]] - (map vector (repeat target-name) (-> machfile (get target-name) (get 'depends)))) - [nil target-name]))))))) +(defrecord Glob [glob]) +(reader/register-tag-parser! "mach/glob" (fn [glob] (->Glob glob))) + +(defprotocol Target + (match? [this machfile])) + +(extend-protocol Target + cljs.core/Symbol + (match? [this target-name] + (= this (symbol target-name))) + Glob + (match? [this target-name] + (mm.isMatch (str target-name) (:glob this)))) + +(defn resolve-target + "Retrieve target-name from machfile as appropriate target with context added" + [machfile target-name] + (when-let [[matcher target] + (or + ;; TODO: Make is smarter about this, e.g. it prefers to + ;; foo.min.js over foo.js when there is a target for both *.js + ;; and *.min.js + ;; We are currently non-deterministic in that case + (some (fn [[k v]] + (when (match? k (str target-name)) + [k v])) + machfile) + ;; Else try to search for product + ;; TODO: Does this make sense anymore now globs are available? + ;; strings can be added for static "products" also + (some (fn [[k v]] + (when (= target-name (get v 'product ::sentinel)) + [k v])) + machfile))] + (assoc target + :mach/_target-ctx target-name + :mach/_matcher-ctx matcher))) + +(defn target-order [machfile target] + (let [deps (tree-seq + (fn [[_ target]] + (and (map? target) + (contains? target 'depends))) + (fn [[_ target]] + (map (fn [target dependency] + [target (resolve-target machfile dependency)]) + (repeat target) + (get target 'depends))) + [nil target]) + ;; We want to use clojure's equality semantics with js, so we must turn + ;; them to something that js can do equality on + lookup (zipmap (into #{} cat deps) + (repeatedly (comp str gensym))) + reverse-lookup (map-invert lookup)] + (->> deps + (map (fn [[k v]] [(lookup k) (lookup v)])) + (clj->js) + toposort + (map reverse-lookup) + rest))) ;; References @@ -211,7 +262,7 @@ (defmulti apply-verb "Return boolean to indicate if work was done (true) or not (false)" - (fn [machfile [target-name target] verb] verb)) + (fn [machfile target verb] verb)) (defmethod apply-verb :default [_ _ verb] (throw (ex-info (str "Unknown verb: '" verb "'") {}))) @@ -254,7 +305,7 @@ ;; We did work so return true true)) -(defmethod apply-verb nil [machfile [target-name target] verb] +(defmethod apply-verb nil [machfile target verb] (if-let [novelty-form (and (map? target) (get target 'novelty))] (let [novelty (eval-rule novelty-form target machfile)] ;; Call update! @@ -266,14 +317,14 @@ (update! machfile target))) ;; Run the update (or produce) and print, no deps -(defmethod apply-verb 'update [machfile [target-name target] verg] +(defmethod apply-verb 'update [machfile target verg] (update! machfile target)) ;; Print the produce -(defmethod apply-verb 'print [machfile [target-name target] verb] +(defmethod apply-verb 'print [machfile target verb] (update! machfile target :post-op (fn [v _] (println v)))) -(defmethod apply-verb 'clean [machfile [target-name target] verb] +(defmethod apply-verb 'clean [machfile target verb] (if-let [rule (get target 'clean!)] ;; If so, call it (eval-rule rule target machfile) @@ -290,59 +341,53 @@ :otherwise false)))) true) -(defmethod apply-verb 'depends [machfile [target-name target] verb] +(defmethod apply-verb 'depends [machfile target verb] (pprint/pprint - (target-order machfile target-name)) + (map :mach/_matcher-ctx (target-order machfile target))) true) -(defmethod apply-verb 'novelty [machfile [target-name target] verb] +(defmethod apply-verb 'novelty [machfile target verb] (pprint/pprint (when-let [novelty (get target 'novelty)] (eval-rule novelty target machfile)))) -(defn resolve-target - "Resolve target key (symbol) matching given target (string) in machfile. - Once a target has been resolved, it is also validated." +(defn resolve-validate-target + "Resolve and validate a target from a machfile" [machfile target-name] - (if-let [target-symbol (or (and (contains? machfile (symbol target-name)) (symbol target-name)) - ;; Else try to search for product - (some (fn [[k v]] - (when (= target-name (get v 'product)) - k)) - machfile))] - (let [target (get machfile target-symbol)] + (if-let [target (resolve-target machfile target-name)] + (do ;; validate target contract: (when (and (get target 'produce) (get target 'update!)) (throw (ex-info "Invalid to have both update! and produce in the same target" {:target target}))) ;; Validate dependency tree: - (doseq [dep-target (rest (target-order machfile target-symbol))] - (when-not (get machfile dep-target) + (doseq [dep-target (rest (target-order machfile target))] + (when-not (resolve-target machfile dep-target) (throw (ex-info (str "Target dependency not found: " dep-target) {})))) - target-symbol) + target) (throw (ex-info (str "Could not resolve target: " target-name) {})))) (defn execute-plan [machfile build-plan] - (into {} (for [[target-symbol verb] build-plan] - [[target-symbol verb] - (apply-verb machfile [target-symbol (get machfile target-symbol)] verb)]))) + (into {} (for [[target verb] build-plan] + [[target verb] + (apply-verb machfile target verb)]))) -(defn build-plan [machfile [target-symbol verb]] +(defn build-plan [machfile [target verb]] (for [dependency-target (case verb nil - (reverse (target-order machfile target-symbol)) + (reverse (target-order machfile target)) 'clean - (target-order machfile target-symbol) + (target-order machfile target) - [target-symbol])] + [target])] [dependency-target verb])) (defn- expand-out-target-and-verbs [machfile target+verbs] (let [[target-name & verbs] (str/split target+verbs ":") - target-symbol (resolve-target machfile target-name)] + target (resolve-validate-target machfile target-name)] (for [verb (if verbs (map symbol verbs) [nil])] - [target-symbol verb]))) + [target verb]))) (defn- preprocess-init [machfile] (when-let [target (get machfile 'mach/init)] @@ -448,13 +493,14 @@ (binding [cljs/*eval-fn* repl/caching-node-eval] (when-not (->> tasks (mapcat (partial expand-out-target-and-verbs machfile)) - (reduce (fn [m target-verb] + (reduce (fn [m [target verb :as target-verb]] (if (contains? m target-verb) - (println (str "mach: '" (if-let [verb (second target-verb)] - (str (first target-verb) ":" verb) (first target-verb)) + (println (str "mach: '" (str (:mach/_matcher-ctx target-verb) + (when verb (str ":" verb))) "' is up to date.")) (let [build-plan (build-plan machfile target-verb)] - (merge m (execute-plan machfile build-plan))))) {}) + (merge m (execute-plan machfile build-plan))))) + {}) (vals) (some identity)) (println "Nothing to do!")))