diff --git a/jank-build/README.md b/jank-build/README.md new file mode 100644 index 0000000..4245546 --- /dev/null +++ b/jank-build/README.md @@ -0,0 +1,10 @@ +# jank-build +[![Clojars Project](https://img.shields.io/clojars/v/org.jank-lang.commons/jank-build.svg)] +Build script helper functions. + +## Usage +Add this project to your Leiningen `:build-dependencies` vector: + +```clojure +[org.jank-lang.commons/jank-build "0.1-SNAPSHOT"] +``` diff --git a/jank-build/project.clj b/jank-build/project.clj new file mode 100644 index 0000000..72422a4 --- /dev/null +++ b/jank-build/project.clj @@ -0,0 +1,9 @@ +(defproject org.jank-lang.commons/jank-build "0.1-SNAPSHOT" + :description "Build script helper functions" + :url "https://github.com/jank-lang/commons" + :license {:name "MPL 2.0" + :url "https://www.mozilla.org/en-US/MPL/2.0/"} + :source-paths ["src" "test"] + :profiles {:dev {:dependencies [[org.clojure/clojure "1.12.6"] + [babashka/fs "0.5.34"] + [babashka/process "0.6.25"]]}}) diff --git a/jank-build/src/jank/build/cmake.clj b/jank-build/src/jank/build/cmake.clj new file mode 100644 index 0000000..37245e2 --- /dev/null +++ b/jank-build/src/jank/build/cmake.clj @@ -0,0 +1,32 @@ +(ns jank.build.cmake + (:require [babashka.process :as proc] + [jank.build.util :as util])) + +(defn install-rpath [] + (when (util/linux?) "$ORIGIN")) + +(defn default-defines [{:keys [out-dir optimization-level static?] + :or {optimization-level 0}}] + (let [rpath (install-rpath)] + (merge {"BUILD_SHARED_LIBS" (if static? "OFF" "ON") + "CMAKE_BUILD_TYPE" (if (pos? optimization-level) "Release" "Debug") + "CMAKE_INSTALL_PREFIX" out-dir + ;; Make sure these paths are standardized, otherwise libs may + ;; sometimes go into lib64/ instea of lib/, etc. + "CMAKE_INSTALL_BINDIR" "bin" + "CMAKE_INSTALL_LIBDIR" "lib" + "CMAKE_INSTALL_INCLUDEDIR" "include"} + (when (and (not static?) rpath) + {"CMAKE_INSTALL_RPATH" rpath})))) + +(defn cmake + "Run the `cmake` tool in the src-dir, building to the intermediate build-dir, + and installing artifacts into the out-dir. + + No jank-build directives are output, so make sure to specify the correct + link/include directories, link libraries, etc. yourself." + [{:keys [src-dir build-dir] :as input} & {:keys [defines target] :or {target "install"}}] + (let [d-flags (map (fn [[k v]] (str "-D" (name k) "=" v)) + (merge (default-defines input) defines))] + (proc/shell (concat ["cmake"] d-flags ["-B" build-dir src-dir])) + (proc/shell ["cmake" "--build" build-dir "--parallel" "--target" target]))) diff --git a/jank-build/src/jank/build/pkg_config.clj b/jank-build/src/jank/build/pkg_config.clj new file mode 100644 index 0000000..3e33311 --- /dev/null +++ b/jank-build/src/jank/build/pkg_config.clj @@ -0,0 +1,66 @@ +(ns jank.build.pkg-config + (:require [clojure.string :as string] + [babashka.process :as proc] + [babashka.fs :as fs] + [jank.build.util :as util])) + +(defn parse-prefixed [prefix s] + (for [entry (string/split s #"\s+") + :when (string/starts-with? entry prefix)] + (subs entry (count prefix)))) + +(defn has-static-lib? [lib link-dirs] + ; TODO: Windows? + (let [lib-name (str "lib" lib ".a")] + (some (fn [link-dir] + (let [link-dir+lib (fs/path link-dir lib-name)] + (when (fs/exists? link-dir+lib) + true))) + link-dirs))) + +(defn brew-pkg-config-path [build-input pkg] + (when (and (util/mac?) (fs/which "brew")) + (let [{:keys [exit out]} (proc/shell {:out :string + :err :string + :continue true + ; Disable brew's cache and use the already-authorized + ; working directory rather than /private/tmp. + :extra-env {"HOMEBREW_NO_BOOTSNAP" "1" + "HOMEBREW_NO_AUTO_UPDATE" "1" + "HOMEBREW_TEMP" (:out-dir build-input)}} + "brew" "--prefix") + prefix (string/trim out)] + (when (and (zero? exit) (not (string/blank? prefix))) + (str prefix "/opt/" pkg "/lib/pkgconfig"))))) + +(defn pkg-config-path + "Rebuild the PKG_CONFIG_PATH string with additional paths prepended. nil + entries are ignored." + [& paths] + (let [sep (System/getProperty "path.separator") + from-env (some-> (System/getenv "PKG_CONFIG_PATH") + (not-empty) + (string/split (re-pattern sep))) + fresh (filter some? paths)] + (string/join sep (concat fresh from-env)))) + +(defn pkg-config + "Call the `pkg-config` tool and parse link directories, include directories, + and link libraries. + + jank-build directives will automatically be output for these values." + [build-input pc-name] + ;; TODO: parse preprocessor defines from cflags + (let [pc-cmd (cond-> ["pkg-config" pc-name "--libs" "--cflags"] + (:static? build-input) (conj "--static")) + brew-path (brew-pkg-config-path build-input pc-name) + pc-opts {:extra-env {"PKG_CONFIG_PATH" (pkg-config-path brew-path)} + :out :string} + pc-output (->> pc-cmd (apply proc/shell pc-opts) :out) + link-dirs (parse-prefixed "-L" pc-output)] + (run! util/link-dir link-dirs) + (run! util/include-dir (parse-prefixed "-I" pc-output)) + (run! #(if (and (:static? build-input) (has-static-lib? % link-dirs)) + (util/link-static-library %) + (util/link-library %)) + (parse-prefixed "-l" pc-output)))) diff --git a/jank-build/src/jank/build/util.clj b/jank-build/src/jank/build/util.clj new file mode 100644 index 0000000..24ff88f --- /dev/null +++ b/jank-build/src/jank/build/util.clj @@ -0,0 +1,54 @@ +(ns jank.build.util + (:require [clojure.string :as string])) + +;; OS identification functions are adapted from the Apache Commons +;; Lang SystemUtils class. +;; +;; https://github.com/apache/commons-lang/blob/32dc4c501e3acba29d53a466eebd611bb2f093a5/src/main/java/org/apache/commons/lang3/SystemUtils.java + +(def os-name (System/getProperty "os.name")) + +(defn linux? [] + (-> (string/lower-case os-name) + (string/starts-with? "linux"))) + +(defn mac? [] + (-> (string/lower-case os-name) + (string/starts-with? "mac"))) + +(defn windows? [] + (-> (string/lower-case os-name) + (string/starts-with? "windows"))) + +;; Emit common "jank-build::"-prefixed items. + +(defn define + ([k] (println (str "jank-build::define=" k))) + ([k v] (println (str "jank-build::define=" k "=" v)))) + +(defn include-dir + "Add a jank include-dir directive (a -I flag). If a relative path is given + then it is assumed to be relative to the build output directory." + [dir] + (println (str "jank-build::include-dir=" dir))) + +(defn link-dir + "Add a jank link-dir directive (a -L flag). If a relative path is given then + it is assumed to be relative to the build output directory." + [dir] + (println (str "jank-build::link-dir=" dir))) + +(defn link-library [lib] + (println (str "jank-build::link-library=" lib))) + +(defn link-static-library [lib] + (println (str "jank-build::link-static-library=" lib))) + +(defn link-framework [framework] + (println (str "jank-build::link-framework=" framework))) + +(defn rerun-if-changed [path] + (println (str "jank-build::rerun-if-changed=" path))) + +(defn rerun-if-env-changed [k] + (println (str "jank-build::rerun-if-env-changed=" k))) diff --git a/jank-build/test-data/cmake-project/CMakeLists.txt b/jank-build/test-data/cmake-project/CMakeLists.txt new file mode 100644 index 0000000..9dae37b --- /dev/null +++ b/jank-build/test-data/cmake-project/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.12) +project(cmake_project VERSION 1.0 LANGUAGES CXX) +add_library(foo foo.cpp) diff --git a/jank-build/test-data/cmake-project/foo.cpp b/jank-build/test-data/cmake-project/foo.cpp new file mode 100644 index 0000000..b85b60d --- /dev/null +++ b/jank-build/test-data/cmake-project/foo.cpp @@ -0,0 +1,4 @@ +int foo() +{ + return 42; +} diff --git a/jank-build/test/jank/build/cmake_test.clj b/jank-build/test/jank/build/cmake_test.clj new file mode 100644 index 0000000..dec4153 --- /dev/null +++ b/jank-build/test/jank/build/cmake_test.clj @@ -0,0 +1,15 @@ +(ns jank.build.cmake-test + (:require [clojure.test :refer [deftest is]] + [clojure.string :as string] + [babashka.fs :as fs] + [jank.build.cmake :as cm])) + +(def test-project-dir (fs/path "test-data/cmake-project")) + +(deftest build-example-project-test + ;; should not throw + (cm/cmake {:src-dir test-project-dir + :build-dir (fs/create-temp-dir {:prefix "jank-build-cmake-test"}) + :out-dir (fs/create-temp-dir {:prefix "jank-build-cmake-test"})} + {:defines {:SOME_DEFINE true} + :target "all"})) diff --git a/jank-build/test/jank/build/pkg_config_test.clj b/jank-build/test/jank/build/pkg_config_test.clj new file mode 100644 index 0000000..d63c509 --- /dev/null +++ b/jank-build/test/jank/build/pkg_config_test.clj @@ -0,0 +1,11 @@ +(ns jank.build.pkg-config-test + (:require [clojure.test :refer [deftest is]] + [clojure.string :as string] + [jank.build.pkg-config :as pc])) + +;; zlib should be on all jank-enabled systems. +(deftest find-zlib-test + (let [out (with-out-str *out* (pc/pkg-config {} "zlib"))] + (is (string/includes? out "jank-build::link-dir=")) + (is (string/includes? out "jank-build::include-dir=")) + (is (string/includes? out "jank-build::link-library=z")))) diff --git a/jank-build/test/jank/build/util_test.clj b/jank-build/test/jank/build/util_test.clj new file mode 100644 index 0000000..c691e8b --- /dev/null +++ b/jank-build/test/jank/build/util_test.clj @@ -0,0 +1,35 @@ +(ns jank.build.util-test + (:require [clojure.test :refer [deftest is]] + [jank.build.util :as util])) + +(deftest os-detection-test + (with-redefs [util/os-name "Linux"] + (is (util/linux?)) + (is (not (util/mac?))) + (is (not (util/windows?)))) + (with-redefs [util/os-name "Mac OS X"] + (is (util/mac?)) + (is (not (util/linux?))) + (is (not (util/windows?)))) + (with-redefs [util/os-name "Windows XP"] + (is (util/windows?)) + (is (not (util/linux?))) + (is (not (util/mac?))))) + +(deftest build-directives-test + (is (= (with-out-str (util/define "KEY")) + "jank-build::define=KEY\n")) + (is (= (with-out-str (util/define "KEY" "VAL")) + "jank-build::define=KEY=VAL\n")) + (is (= (with-out-str (util/include-dir "/usr/include")) + "jank-build::include-dir=/usr/include\n")) + (is (= (with-out-str (util/link-dir "/usr/lib64")) + "jank-build::link-dir=/usr/lib64\n")) + (is (= (with-out-str (util/link-library "m")) + "jank-build::link-library=m\n")) + (is (= (with-out-str (util/link-framework "Cocoa")) + "jank-build::link-framework=Cocoa\n")) + (is (= (with-out-str (util/rerun-if-changed "/home")) + "jank-build::rerun-if-changed=/home\n")) + (is (= (with-out-str (util/rerun-if-env-changed "PATH")) + "jank-build::rerun-if-env-changed=PATH\n")))