├── .buckconfig ├── .gitignore ├── .travis.yml ├── BUCK ├── LICENSE ├── README.md ├── build.cljs ├── clj-cljs-config ├── BUCK.example ├── config.py ├── figwheel-index.html ├── project-clj.clj ├── project-cljs.clj └── project-repl.clj ├── lib.py ├── tester-lein-clj.sh ├── tester-lein-cljc-doo.sh ├── tester-lein-cljc-planck.sh ├── tester-lein-cljs-doo.sh ├── tester-lein-cljs-planck.sh └── tests ├── BUCK.tests ├── a_clj.clj ├── a_cljc.cljc ├── a_cljs.cljs ├── b.cljs ├── b_clj.clj ├── b_cljc.cljc ├── b_cljs.cljs ├── c.cljc ├── c_clj.clj ├── c_cljc.cljc ├── c_cljs.cljs ├── d1.clj ├── d1.cljc ├── d1.cljs ├── d2.clj ├── d2.cljc ├── d2.cljs ├── e_clj.clj ├── e_cljc.cljc ├── e_cljs.cljs ├── e_test.clj ├── e_test.cljc ├── e_test.cljs ├── f_clj.clj ├── f_cljc.cljc ├── f_cljs.cljs ├── g1_test.clj ├── g1_test.cljc ├── g1_test.cljs ├── g2_test.clj ├── g2_test.cljc ├── g2_test.cljs ├── g_clj.clj ├── g_cljc.cljc ├── g_cljs.cljs └── gen-out.sh /.buckconfig: -------------------------------------------------------------------------------- 1 | [buildfile] 2 | includes = //clj-cljs-config/config.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *jar 4 | /lib/ 5 | /classes/ 6 | /target/ 7 | /checkouts/ 8 | .lein-deps-sum 9 | .lein-repl-history 10 | .lein-plugins/ 11 | .lein-failures 12 | .nrepl-port 13 | buck-out 14 | .buckd 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | cache: 3 | directories: 4 | - /Library/Caches/Homebrew 5 | - .m2 6 | install: 7 | - brew update && brew tap facebook/fb 8 | - brew install buck leiningen phantomjs planck 9 | script: 10 | - mv tests/BUCK.tests tests/BUCK 11 | - mv clj-cljs-config/BUCK.example clj-cljs-config/BUCK 12 | - TERM=dumb buck build "//..." && TERM=dumb buck test "//..." 13 | -------------------------------------------------------------------------------- /BUCK: -------------------------------------------------------------------------------- 1 | export_file(name = 'build.cljs', 2 | visibility = ['PUBLIC']) 3 | 4 | genrule(name = 'builder-planck', 5 | srcs = [], 6 | # HACK: If we change `build.cljs` file then `build` target would be updated, but because 7 | # it will generate exactly the same output (planck script invocation is the same) then 8 | # no rebuild will occur for dependent targets. Here we force it by including md5 checksum 9 | # into the script file as a comment 10 | # Note: --auto-cache --cache=$SRCDIR --static-fns optimisations doesn't bring anything 11 | bash = 'echo "planck $(location :build.cljs) \$@" > $OUT && ' + 12 | 'echo "#`cat $(location :build.cljs) | md5`" >> $OUT && ' + 13 | 'chmod +x $OUT', 14 | out = 'build.sh', 15 | executable = True, 16 | visibility = ['PUBLIC']) 17 | 18 | for name in ['tester-lein-clj.sh', 19 | 'tester-lein-cljs-doo.sh', 20 | 'tester-lein-cljs-planck.sh', 21 | 'tester-lein-cljc-doo.sh', 22 | 'tester-lein-cljc-planck.sh']: 23 | export_file(name = name.split('.')[0], 24 | src = name, 25 | out = name, 26 | visibility = ['PUBLIC']) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Artem Yarulin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clojure-ClojureScript-Buck 2 | 3 | [](https://travis-ci.org/artemyarulin/clojure-clojurescript-buck) 4 | 5 | Clojure and ClojureScript support for [Buck build system](https://buckbuild.com). If you have Clojure/ClojureScript and monorepo then it's a thing to check 6 | 7 | ## Features 8 | 9 | - Build Clojure/ClojureScript 10 | - Run tests for Clojure and choose what test runner to use with ClojureScript: [doo](https://github.com/bensu/doo) or [Planck](http://planck-repl.org/testing.html)! 11 | - Build and test any Clojure/ClojureScript with always the same command: `buck build [module-name] && buck test [module-name]-test` 12 | - Run REPL into the module without too much thinking about details: `buck run [module-name]-repl` 13 | - All the features from [Buck build system](https://buckbuild.com) - it's a peace of cake! 14 | 15 | ## What problems does it solve 16 | 17 | - It allows you to use Clojure/ClojureScript inside a monorepo where different projects may depends on each other 18 | - With project based approach where you have one Leningen/Boot project per application/library it's sometimes difficult to reuse some part of the code between projects. Buck instead encourages you to create many small independent modules with their own dependencies/source/tests which will improve your code reuse 19 | - Buck allows to abstract build/test steps into functions that can be used later on across your repo 20 | 21 | ## Installation and requirements 22 | 23 | Currently only MacOS is supported. Linux/Windows support is covered in this [issue 18](https://github.com/artemyarulin/clojure-clojurescript-buck/issues/18) 24 | 25 | - Put content of this repo to the right place in your monorepo 26 | - Rename `clj-cljs-config/BUCK.example` to `clj-cljs-config/BUCK` to make it processable by Buck 27 | - Change `.buckconfig` so that it will include your config file by default: 28 | 29 | ``` 30 | [buildfile] 31 | includes = //path/to-config/clj-cljs-config/config.py 32 | ``` 33 | 34 | Once it's done check `clj-cljs-config/config.py` - most likely you gonna need to change paths. Feel free to add any additional build logic there 35 | 36 | ## Configuration 37 | 38 | Idea it that `clj_cljs_module` is low level function and it's used always via some wrapper functions like in [RULES/clj-cljs-config/config.py](RULES/clj-cljs-config/config.py) where you can specify custom project files, default dependencies (Clojure/ClojureScript versions), different builders or testers 39 | 40 | ## How does it work 41 | 42 | Under the hood we simply create Leiningen project and put files and parameters in the right place. 43 | 44 | Entry point would be your [custom wrapper](RULES/clj-cljs-config/config.py) with supplied project file on top of `clj_cljs_module` function in [lib.py](RULES/clj-cljs/lib.py), which in turn will save all supplied parameters to `info` file which then would be executed by builder. For now only Planck based [builder.cljs](RULES/clj-cljs/build.cljs) is available. 45 | 46 | `builder` then: 47 | - Create a normal folder structure (with source files placed in a right sub-folder, etc.) 48 | - Collect all sub-dependencies 49 | - Create entry point file which requires all the existing module namespaces (including tests) which simplifies REPL and testing 50 | - Update project file with actual data 51 | 52 | ## Status 53 | 54 | Foundation is solid and unlikely that API gonna change in near feature. It's a second big rewrite already so most of the edge cases should be covered. Although it still missing some important things like [Figwheel support](https://github.com/artemyarulin/clojure-clojurescript-buck/issues/19) 55 | 56 | ## Alternatives 57 | 58 | - [lein-monolith](https://github.com/amperity/lein-monolith) from Amperity - is a Leiningen plugin to work with multiple projects inside a monorepo. Doesn't require any additional tools but Leiningen, much easier to start with, although it still uses project approach. 59 | 60 | - [Ladder developer mentioned on HN](https://news.ycombinator.com/item?id=11507975) that they have their own solution for CLJ/CLJS + Buck which looks awesome but not yet open sourced and includes some hacks in CLJS compiler. 61 | 62 | - [make](https://www.gnu.org/software/make/) - there are no tasks that you cannot do with make. If you like bare metal - then check [version 1.0.0](https://github.com/artemyarulin/clojure-clojurescript-buck/tree/1.0.0), it was implemented with power of shell,sed,grep and regexps. 63 | -------------------------------------------------------------------------------- /build.cljs: -------------------------------------------------------------------------------- 1 | (ns build.core 2 | (:require [planck.core :as core] 3 | [planck.io :as io] 4 | [planck.shell :as shell] 5 | [cljs.tools.reader :as reader] 6 | [clojure.string :as string])) 7 | 8 | ;; Helpers 9 | (def make-dirs (partial shell/sh "mkdir" "-p")) 10 | (def pwd (-> "pwd" shell/sh :out string/trim)) 11 | (defn copy [from to] (shell/sh "cp" "-r" from to)) 12 | (defn symlink [from to] (shell/sh "ln" from to)) 13 | (defn path-join [& args] (string/join "/" args)) 14 | (defn delete-last-path-component [p] (-> p (string/split "/") butlast (#(string/join "/" %)))) 15 | (defn delete-path [path] (shell/sh "rm" "-rf" path)) 16 | (defn pad-left [s len char] (if (< (count s) len) (recur (str char s) len char) s)) 17 | 18 | (defn path-from-content-namespace 19 | "Given basepath and file content will parse content and return new 20 | path based on file namespace" 21 | [base-path file-content] 22 | (letfn [(is-ns? [form] (and (list? form) (= (first form) 'ns) (< 1 (count form)))) 23 | (parse-ns [form] (cond (is-ns? form) (-> form second str) 24 | (list? form) (some parse-ns form)))] 25 | (->> file-content 26 | (#(str "(\n" % "\n)")) ;; Workaround if content has more than one top level s-exp, otherwise read-string will return only first one 27 | (reader/read-string {:read-cond :allow :features #{:clj :cljs}}) 28 | parse-ns 29 | (#(string/split % ".")) 30 | butlast 31 | (#(apply path-join base-path %))))) 32 | 33 | (defn organize-sources 34 | "Given source files and destination will go though all source files 35 | and exec copy-cmd for each file with original and new path which 36 | will be created based on a file namespace" 37 | [base-path files to copy-cmd] 38 | (letfn [(copy-source [source-file path-to] 39 | (let [source-path (.-path source-file) 40 | source-name (-> source-path (string/split "/") last)] 41 | (make-dirs path-to) 42 | (copy-cmd source-path (path-join path-to source-name)))) 43 | (find-out-path [file] 44 | (if (->> file :path (re-find #"clj$|cljs$|cljc$")) 45 | (->> file core/slurp (path-from-content-namespace to)) 46 | (-> file :path (string/replace base-path "") (string/split "/") rest butlast 47 | ((fn[path-parts] 48 | ;; HACK: If we would use clj_module(src=glob(['src/**/*'])) then Buck would copy 49 | ;; everything under src folder, but root folder would be still src, same for tests. 50 | ;; So here we just flatten folders together in order to avoid paths like module/src/src/file 51 | (if (= (first path-parts) (last (string/split to "/"))) 52 | (rest path-parts) 53 | path-parts))) 54 | (#(apply path-join to %)))))] 55 | (->> files 56 | (filter #(not (io/directory? %))) 57 | (mapv #(->> % find-out-path (copy-source %)))))) 58 | 59 | (defn organize-deps 60 | "Read deps file looking for sub-dependencies and merge all of them 61 | back into deps file" 62 | [deps-file] 63 | (letfn [(read-subdeps [path] 64 | (let [subdep-file (path-join path "deps")] 65 | (if (io/file-attributes subdep-file) 66 | (-> subdep-file core/slurp string/split-lines) 67 | [])))] 68 | (->> (core/slurp deps-file) 69 | string/split-lines 70 | (map read-subdeps) 71 | (apply concat) 72 | distinct 73 | (string/join "\n") 74 | (core/spit deps-file)))) 75 | 76 | (defn merge-deps-src 77 | "Merge deps source into current module src folder" 78 | [deps-file to] 79 | (->> (core/slurp deps-file) 80 | string/split-lines 81 | (mapv #(copy (path-join % "src") to)))) 82 | 83 | (defn update-project-file 84 | "Updates project file and replace tokens there with supplied data" 85 | [name main path] 86 | (let [project-file (path-join path "project.clj")] 87 | (-> (core/slurp project-file) 88 | (string/replace "{{name}}" name) 89 | (string/replace "{{main}}" main) 90 | (string/replace "{{deps}}" (core/slurp (path-join path "deps"))) 91 | (#(core/spit project-file %))))) 92 | 93 | (defn ensure-main-exists 94 | "Creates entry point file which requires all the existing module 95 | namespaces (including tests) which simplifies REPL and testing. Used 96 | as main if no main was supplied" 97 | [main path type] 98 | (let [def-main "module.core" 99 | find-all-namespaces (fn[path] 100 | (->> (shell/sh "find" path "-type" "f" "-name" "*.cljc" "-o" "-name" (str "*." type)) 101 | :out 102 | string/split-lines 103 | (map #(-> % 104 | (string/replace path "") 105 | (string/replace "/" ".") 106 | (string/replace "_" "-") 107 | (string/split ".") 108 | butlast 109 | rest)) 110 | (map #(string/join "." %)))) 111 | main-file (fn[namespaces] 112 | (str "(ns " def-main " (:require " 113 | (string/join "\n" (map #(str "[" % "]") namespaces)) 114 | "))")) 115 | main-path (path-join path "src" "module")] 116 | (make-dirs main-path) 117 | (->> (concat (find-all-namespaces (path-join path "src")) 118 | (find-all-namespaces (path-join path "test"))) 119 | (filter (complement string/blank?)) 120 | (filter (partial not= "deps")) 121 | main-file 122 | (core/spit (path-join main-path (str "core." type)))) 123 | (if (string/blank? main) 124 | def-main 125 | main))) 126 | 127 | (defn get-all-project-deps [] 128 | (shell/sh "buck" "build" "//ext:") ;; Ensure that all exts got built first 129 | (->> (shell/sh "buck" "targets" "//ext:" "--show-output" "--verbose" "0") 130 | :out 131 | string/split-lines 132 | (map #(string/split % " ")) 133 | (map second) 134 | (map #(path-join pwd % "deps")) 135 | (map core/slurp) 136 | string/join)) 137 | 138 | (defn run-repl [args] 139 | (let [[_ project-file resource output-folder query] args 140 | targets (->> query (shell/sh "buck" "query") :out string/split-lines) 141 | source-dest-path (path-join pwd output-folder "src") 142 | resource-dest-path (path-join pwd output-folder "resources")] 143 | (delete-path source-dest-path) 144 | (delete-path resource-dest-path) 145 | (loop [targets' targets counter 1] 146 | (when-let [target (first targets')] 147 | (println (str "[" (-> counter str (pad-left (-> targets count str count) " ")) "/" (count targets) "] Linking " target)) 148 | (let [buildfile-path (->> (shell/sh "buck" "query" (str "buildfile('" target "')")) :out (path-join pwd) delete-last-path-component) 149 | target-files (->> (shell/sh "buck" "query" (str "labels(srcs,deps('" target "'))")) :out string/split-lines (map #(path-join pwd %)))] 150 | (organize-sources buildfile-path 151 | (map io/file target-files) 152 | source-dest-path 153 | symlink) 154 | (organize-sources buildfile-path 155 | (->> target-files 156 | (filter #(not (re-find #"clj$|cljs$|cljc$" %))) 157 | (map io/file)) 158 | resource-dest-path 159 | symlink)) 160 | (recur (rest targets') (inc counter)))) 161 | (organize-sources (delete-last-path-component resource) [(io/file resource)] (path-join resource-dest-path "public") symlink) 162 | (ensure-main-exists nil output-folder "cljs") 163 | (-> (core/slurp project-file) 164 | (string/replace "{{deps}}" (get-all-project-deps)) 165 | (#(core/spit (path-join output-folder "project.clj") %))))) 166 | 167 | (let [args core/*command-line-args*] 168 | (case (first args) 169 | "repl" (run-repl args) 170 | ;; We cannot run Buck commands while we are inside a command which is running by Buck again 171 | ;; As a workaround we print the actual command to execute, so we can still use it like 172 | ;; $(buck run repl -- "//...") 173 | "repl-init" (print (string/join " " (apply vector (nth args 1) "repl" (subvec (vec args) 2)))) 174 | (let [parse-args #(let [info-file (-> % first core/slurp string/trim)] 175 | (zipmap [:name :type :main :src :out :task] 176 | (conj (string/split info-file ";") (second %)))) 177 | {:keys [src out type task name main]} (parse-args args) 178 | build? (= task "build")] 179 | (organize-sources src (core/file-seq src) (path-join out (if build? "src" "test")) symlink) 180 | (merge-deps-src (path-join out "deps") out) 181 | (organize-deps (path-join out "deps")) 182 | (update-project-file name (if build? name (ensure-main-exists main out type)) out)))) 183 | -------------------------------------------------------------------------------- /clj-cljs-config/BUCK.example: -------------------------------------------------------------------------------- 1 | # Project files 2 | for name in ['project-clj.clj','project-cljs.clj','project-repl.clj']: 3 | export_file(name = name.split('.')[0], 4 | src = name, 5 | out = name, 6 | visibility = ['PUBLIC']) 7 | 8 | # Figwheel starting point 9 | export_file(name = 'figwheel-index', 10 | src = 'figwheel-index.html', 11 | out = 'index.html', 12 | visibility = ['PUBLIC']) 13 | 14 | clj_cljs_repl(name = 'repl', 15 | builder = '//:builder-planck', 16 | project_file = ':project-repl', 17 | resource = ':figwheel-index', 18 | output_folder = '.repl') 19 | -------------------------------------------------------------------------------- /clj-cljs-config/config.py: -------------------------------------------------------------------------------- 1 | # Here goes example wrappers that you are free to modify depending on 2 | # your needs. It's not meant to be updated with new releases of 3 | # clj-cljs-buck: Use it as an example, change it and store in your 4 | # repo 5 | 6 | include_defs('//lib.py') 7 | 8 | # Set of helpers 9 | def ext(name): 10 | return '//tests:' + name 11 | def executer(name): 12 | return '//:' + name 13 | def resource(name): 14 | return '//clj-cljs-config:' + name 15 | def ensure_list(i): 16 | return i if isinstance(i,list) else [i] 17 | cljs_deps = [ext('org.clojure/clojure'), 18 | ext('org.clojure/clojurescript'), 19 | ext('figwheel-sidecar'), 20 | ext('com.cemerick/piggieback')] 21 | builder = executer('builder-planck') 22 | 23 | def module(ext,project_file,name,src,main,resources,modules,tests,test_resources,test_modules,tester,tester_args=[]): 24 | """Module creator helper. ensure_list that ensures that item is a list 25 | or wraps item with it. Most often you want to use one source file, 26 | one test, one module dependency so you can use 27 | clj_module(src='a.clj',tests='test.clj',modules=':b') without 28 | wrapping each with []. Also allows specifiying module without 29 | source files. name + ext will be used instead 30 | """ 31 | src = [name.replace('-','_') + '.' + ext] if src == None else ensure_list(src) 32 | 33 | clj_cljs_module(ext = ext, 34 | project_file = resource(project_file), 35 | builder = builder, 36 | name = name, 37 | resources = ensure_list(resources), 38 | src = src, 39 | modules = ensure_list(modules), 40 | main = main) 41 | if tests: 42 | clj_cljs_module(ext = ext, 43 | project_file = resource(project_file), 44 | builder = builder, 45 | name = name, 46 | resources = ensure_list(test_resources), 47 | src = ensure_list(tests), 48 | modules = ensure_list(test_modules) + [':' + name], 49 | main = main, 50 | tester = tester, 51 | tester_args = tester_args) 52 | 53 | def clj_module(name,src=None,modules=[],main=None,tests=[],test_modules=[],resources=[],test_resources=[]): 54 | """First is CLJ wrapper - nothing fancy, we just predefine what 55 | project file to use, setup tester (which is an only options for 56 | CLJ for now) and add Clojure as dependency that is added always. 57 | """ 58 | module('clj', 59 | 'project-clj', 60 | name, 61 | src, 62 | main, 63 | resources, 64 | ensure_list(modules) + [ext('org.clojure/clojure')], 65 | tests, 66 | test_resources, 67 | test_modules, 68 | executer('tester-lein-clj')) 69 | 70 | def cljs_module(name,src=None,modules=[],main=None,tests=[],test_modules=[],resources=[],test_resources=[],itests=[]): 71 | """Here goes CLJS wrapper with example of custom logic - if tests 72 | supplied than planck test executer is used (because it's 10 times 73 | faster!), otherwise classic doo. Another custom logic is release 74 | task - if main is specified then new target is added which creates 75 | release bundle 76 | """ 77 | module('cljs', 78 | 'project-cljs', 79 | name, 80 | src, 81 | main, 82 | resources, 83 | ensure_list(modules) + cljs_deps, 84 | ensure_list(tests) + ensure_list(itests), 85 | test_resources, 86 | test_modules, 87 | executer('tester-lein-cljs-doo') if itests else executer('tester-lein-cljs-planck')) 88 | 89 | if (main): 90 | genrule(name + '-release', 91 | srcs = [], 92 | bash = 'mkdir $OUT && cd $(location :{0}) && lein cljsbuild once release && cp release/{0}.js $OUT && cp -r resources $OUT'.format(name), 93 | out = 'build') 94 | 95 | def cljc_module(name,src=None,modules=[],main=None,tests=[],test_modules=[],resources=[],test_resources=[],itests=[]): 96 | module('cljc', 97 | 'project-cljs', 98 | name, 99 | src, 100 | main, 101 | resources, 102 | ensure_list(modules) + cljs_deps, 103 | ensure_list(tests) + ensure_list(itests), 104 | test_resources, 105 | test_modules, 106 | executer('tester-lein-cljs-doo') if itests else executer('tester-lein-cljs-planck')) 107 | 108 | -------------------------------------------------------------------------------- /clj-cljs-config/figwheel-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |