├── .clj-kondo ├── config.edn └── imports │ ├── babashka │ └── fs │ │ └── config.edn │ ├── http-kit │ └── http-kit │ │ ├── config.edn │ │ └── httpkit │ │ └── with_channel.clj │ ├── lread │ └── status-line │ │ └── config.edn │ ├── rewrite-clj │ └── rewrite-clj │ │ └── config.edn │ └── taoensso │ └── encore │ ├── config.edn │ └── taoensso │ └── encore.clj ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bb.edn ├── build-helper ├── deps.edn └── src │ └── build_helper │ └── core.clj ├── build.clj ├── build_shared.clj ├── deps.edn ├── resources └── clj-easy │ └── graal-build-time-version.txt ├── scripts ├── bump_version.clj └── lint.clj ├── src └── clj_easy │ └── graal_build_time │ ├── InitClojureClasses.java │ └── packages.clj ├── test-hello-world ├── bb.edn ├── build.clj ├── deps.edn ├── lib1 │ ├── bb.edn │ ├── build.clj │ ├── deps.edn │ └── src │ │ └── gbt_test_org │ │ └── p2 │ │ └── core.clj ├── lib2 │ ├── bb.edn │ ├── build.clj │ ├── deps.edn │ └── src │ │ └── gbt_test_org │ │ └── core.clj └── src │ ├── hello │ └── core.clj │ └── hello_world │ └── main.clj ├── test-single-segment ├── bb.edn ├── build.clj ├── deps.edn └── src │ └── single_segment │ └── main.clj └── test └── clj_easy └── graal_build_time └── packages_test.clj /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:config-paths ^:replace [] ;; don't include user configs 2 | } 3 | -------------------------------------------------------------------------------- /.clj-kondo/imports/babashka/fs/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {babashka.fs/with-temp-dir clojure.core/let}} 2 | -------------------------------------------------------------------------------- /.clj-kondo/imports/http-kit/http-kit/config.edn: -------------------------------------------------------------------------------- 1 | 2 | {:hooks 3 | {:analyze-call {org.httpkit.server/with-channel httpkit.with-channel/with-channel}}} 4 | -------------------------------------------------------------------------------- /.clj-kondo/imports/http-kit/http-kit/httpkit/with_channel.clj: -------------------------------------------------------------------------------- 1 | (ns httpkit.with-channel 2 | (:require [clj-kondo.hooks-api :as api])) 3 | 4 | (defn with-channel [{node :node}] 5 | (let [[request channel & body] (rest (:children node))] 6 | (when-not (and request channel) (throw (ex-info "No request or channel provided" {}))) 7 | (when-not (api/token-node? channel) (throw (ex-info "Missing channel argument" {}))) 8 | (let [new-node 9 | (api/list-node 10 | (list* 11 | (api/token-node 'let) 12 | (api/vector-node [channel (api/vector-node [])]) 13 | request 14 | body))] 15 | 16 | {:node new-node}))) 17 | -------------------------------------------------------------------------------- /.clj-kondo/imports/lread/status-line/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {lread.status-line/line clojure.tools.logging/infof 2 | lread.status-line/die clojure.tools.logging/infof}} 3 | -------------------------------------------------------------------------------- /.clj-kondo/imports/rewrite-clj/rewrite-clj/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as 2 | {rewrite-clj.zip/subedit-> clojure.core/-> 3 | rewrite-clj.zip/subedit->> clojure.core/->> 4 | rewrite-clj.zip/edit-> clojure.core/-> 5 | rewrite-clj.zip/edit->> clojure.core/->>}} 6 | -------------------------------------------------------------------------------- /.clj-kondo/imports/taoensso/encore/config.edn: -------------------------------------------------------------------------------- 1 | {:hooks 2 | {:analyze-call 3 | {taoensso.encore/defalias taoensso.encore/defalias 4 | taoensso.encore/defn-cached taoensso.encore/defn-cached 5 | taoensso.encore/defonce taoensso.encore/defonce}}} 6 | -------------------------------------------------------------------------------- /.clj-kondo/imports/taoensso/encore/taoensso/encore.clj: -------------------------------------------------------------------------------- 1 | (ns taoensso.encore 2 | "I don't personally use clj-kondo, so these hooks are 3 | kindly authored and maintained by contributors. 4 | PRs very welcome! - Peter Taoussanis" 5 | (:refer-clojure :exclude [defonce]) 6 | (:require 7 | [clj-kondo.hooks-api :as hooks])) 8 | 9 | (defn defalias 10 | [{:keys [node]}] 11 | (let [[sym-raw src-raw] (rest (:children node)) 12 | src (or src-raw sym-raw) 13 | sym (if src-raw sym-raw (symbol (name (hooks/sexpr src))))] 14 | {:node 15 | (with-meta 16 | (hooks/list-node 17 | [(hooks/token-node 'def) 18 | (hooks/token-node (hooks/sexpr sym)) 19 | (hooks/token-node (hooks/sexpr src))]) 20 | (meta src))})) 21 | 22 | (defn defn-cached 23 | [{:keys [node]}] 24 | (let [[sym _opts binding-vec & body] (rest (:children node))] 25 | {:node 26 | (hooks/list-node 27 | (list 28 | (hooks/token-node 'def) 29 | sym 30 | (hooks/list-node 31 | (list* 32 | (hooks/token-node 'fn) 33 | binding-vec 34 | body))))})) 35 | 36 | (defn defonce 37 | [{:keys [node]}] 38 | ;; args = [sym doc-string? attr-map? init-expr] 39 | (let [[sym & args] (rest (:children node)) 40 | [doc-string args] (if (and (hooks/string-node? (first args)) (next args)) [(hooks/sexpr (first args)) (next args)] [nil args]) 41 | [attr-map init-expr] (if (and (hooks/map-node? (first args)) (next args)) [(hooks/sexpr (first args)) (fnext args)] [nil (first args)]) 42 | 43 | attr-map (if doc-string (assoc attr-map :doc doc-string) attr-map) 44 | sym+meta (if attr-map (with-meta sym attr-map) sym) 45 | rewritten 46 | (hooks/list-node 47 | [(hooks/token-node 'clojure.core/defonce) 48 | sym+meta 49 | init-expr])] 50 | 51 | {:node rewritten})) 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test and Deploy 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - name: Setup Java 13 | uses: actions/setup-java@v4 14 | with: 15 | distribution: 'temurin' 16 | java-version: '21' 17 | 18 | - name: Setup Babashka 19 | uses: DeLaGuardo/setup-clojure@13.2 20 | with: 21 | bb: 'latest' 22 | 23 | - name: Apply Cache 24 | uses: actions/cache@v4 25 | with: 26 | path: | 27 | ~/.m2/repository 28 | ~/.deps.clj 29 | ~/.gitlibs 30 | key: lint-graal-build-time-${{ hashFiles('deps.edn','bb.edn') }} 31 | restore-keys: "lint-graal-build-time-" 32 | 33 | - name: Lint 34 | run: bb lint 35 | 36 | test: 37 | runs-on: ${{ matrix.os }}-latest 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | os: 42 | - 'ubuntu' 43 | - 'windows' 44 | distribution: 45 | - 'graalvm' 46 | - 'graalvm-community' 47 | java-version: 48 | - '24' 49 | steps: 50 | - name: Checkout code 51 | uses: actions/checkout@v4 52 | 53 | - name: Setup GraalVM 54 | uses: graalvm/setup-graalvm@v1 55 | with: 56 | java-version: ${{ matrix.java-version }} 57 | distribution: ${{ matrix.distribution }} 58 | github-token: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | - name: Setup Babashka 61 | uses: DeLaGuardo/setup-clojure@13.2 62 | with: 63 | bb: 'latest' 64 | 65 | - name: Tools versions 66 | run: | 67 | java --version 68 | bb --version 69 | 70 | - name: Apply Cache 71 | uses: actions/cache@v4 72 | with: 73 | path: | 74 | ~/.m2/repository 75 | ~/.deps.clj 76 | ~/.gitlibs 77 | key: ${{ runner.os }}-graal-build-time-${{ hashFiles('**/deps.edn') }} 78 | restore-keys: ${{ runner.os }}-graal-build-time- 79 | 80 | - name: Run tests 81 | run: bb test 82 | 83 | - name: Run native tests 84 | run: bb native-image-test 85 | 86 | deploy: 87 | needs: [lint, test] 88 | if: github.ref == 'refs/heads/main' 89 | runs-on: ubuntu-latest 90 | 91 | steps: 92 | - name: Checkout code 93 | uses: actions/checkout@v4 94 | 95 | - name: Setup Babashka 96 | uses: DeLaGuardo/setup-clojure@13.2 97 | with: 98 | bb: 'latest' 99 | 100 | # any JDK > JDK8 will do to compile sources, but let's deliberately choose one 101 | - name: Setup Java 102 | uses: actions/setup-java@v4 103 | with: 104 | distribution: 'temurin' 105 | java-version: '21' 106 | 107 | - name: Tools versions 108 | run: | 109 | java --version 110 | bb --version 111 | 112 | - name: Apply Cache 113 | uses: actions/cache@v4 114 | with: 115 | path: | 116 | ~/.m2/repository 117 | ~/.deps.clj 118 | ~/.gitlibs 119 | key: ${{ runner.os }}-graal-build-time-${{ hashFiles('**/deps.edn') }} 120 | restore-keys: ${{ runner.os }}-graal-build-time- 121 | 122 | - name: Deploy to clojars 123 | env: 124 | CLOJARS_USERNAME: ${{ secrets.CLOJARS_ORG_USERNAME }} 125 | CLOJARS_PASSWORD: ${{ secrets.CLOJARS_ORG_PASSWORD }} 126 | run: bb deploy 127 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .cpcache 3 | .clj-kondo/.cache 4 | .lsp 5 | !.lsp/config.edn 6 | .nrepl-port 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## Unreleased 5 | 6 | - Add license (and other common entries) to library pom file [#44](https://github.com/clj-easy/graal-build-time/issues/44) 7 | - Minor 8 | - Build, test and release `graal-build-time` with current version of Clojure [#46](https://github.com/clj-easy/graal-build-time/issues/46) 9 | 10 | ## v1.0.5 11 | 12 | - ⚠️ _**You'll need to update your `native-image` command line**_ ⚠️ - Adapt to GraalVM deprecation 13 | - The Graal team has deprecated automatic discovery of GraalVM `native-image` `Feature` classes. They feel it is safer for you to explicitly opt in when enabling features. 14 | - Since `graal-build-time` was implemented as an auto-discovered Feature class, we have adapted to this change. 15 | - After upgrading, you need to add `--features=clj_easy.graal_build_time.InitClojureClasses` to your `native-image` command line. 16 | 17 | ## v0.1.4 18 | 19 | - Our output lines during a `native-image` build are now identified with prefix `[clj-easy/graal-build-time]` [#18](https://github.com/clj-easy/graal-build-time/pull/18) 20 | - Child package `hello.world` from classpath element x is now properly excluded when parent package `hello` occurs in classpath element y [#17](https://github.com/clj-easy/graal-build-time/pull/17) 21 | - Add source control management (scm) info within our jar so that clojars and others can point back to our github repo [#19](https://github.com/clj-easy/graal-build-time/pull/19) 22 | 23 | ## v0.1.3 24 | 25 | - Package `hello_world` is no longer excluded due to erroneously being considered a child of package `hello` [#12](https://github.com/clj-easy/graal-build-time/pull/12) 26 | 27 | ## v0.1.2 28 | 29 | - Stop asking `native-image` to register blank packages, it ascribes this to mean a request to register all packages [#11](https://github.com/clj-easy/graal-build-time/pull/11) 30 | - Properly analyze `clojure.*` packages on Windows when working from jar file [#11](https://github.com/clj-easy/graal-build-time/pull/11) 31 | 32 | ## v0.1.0 33 | 34 | - First 0.1.x release! 35 | 36 | ## v0.0.x 37 | 38 | - Initial test releases 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 CLJ Easy 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 | [![Clojars Project](https://img.shields.io/clojars/v/com.github.clj-easy/graal-build-time.svg)](https://clojars.org/com.github.clj-easy/graal-build-time) 2 | [![Slack community](https://img.shields.io/badge/Slack-chat-blue?style=flat-square)](https://clojurians.slack.com/archives/C02DQFVS0MC) 3 | 4 | # graal-build-time 5 | 6 | Automatically detect and initialize Clojure classes during `native-image` compilation. 7 | 8 | > ⬆️ **Upgrading to from v0 to v1?**
9 | > Add `--features=clj_easy.graal_build_time.InitClojureClasses` to your `native-image` command line. 10 | 11 | We compile our Clojure sources to `.class` files so that GraalVM `native-image` can in turn compile them into a binary executable. 12 | Due to their nature, Clojure `.class` files typically must be initialized at build time by GraalVM `native-image`. 13 | If they are not initialized at build time, GraalVM will still create your binary executable, but when you run it, it probably won't work. 14 | You will likely see it fail with an error that includes: 15 | ``` 16 | java.io.FileNotFoundException: Could not locate clojure/core__init.class, clojure/core.clj or clojure/core.cljc on classpath 17 | ``` 18 | 19 | In the early days, our solution was to use `native-image`'s `--initialize-at-build-time` to globally initialize all classes at build time. 20 | Because global initialization of all classes can be problematic, GraalVM deprecated this usage. 21 | Instead, you must be explicit and specify the packages of the classes you need to initialize at build time. 22 | For example: `--initialize-at-build-time=clojure,my_library,etc...` 23 | But, this can be tedious and error-prone. 24 | 25 | Hence, `graal-build-time`. 26 | This library automatically detects `.class` files created by Clojure and asks `native-image` to initialize them at build time. 27 | 28 | ## Usage 29 | 30 | We assume you are using the current stable release of: 31 | - [GraalVM Community Edition](https://github.com/graalvm/graalvm-ce-builds/releases/) or [GraalVM Oracle](https://www.graalvm.org/downloads/) 32 | - [Clojure](https://clojure.org/guides/install_clojure) 33 | 34 | We don't test against or support older releases. 35 | 36 | For your `native-image` build: 37 | 1. If you are using `--initialize-at-build-time`, remove it. 38 | 2. Add `--features=clj_easy.graal_build_time.InitClojureClasses` 39 | 3. Include the `graal-build-time` library on your classpath. 40 | This is typically done by adding this library to your project dependencies (see the clojars link above). 41 | 42 | During the `native-image` build process, you will see a line of output from `graal-build-time` describing the packages it has detected: 43 | 44 | ``` 45 | [clj-easy/graal-build-time] Registering packages for build time initialization: clojure, clj_easy.graal_build_time 46 | ``` 47 | 48 | ## How it works 49 | 50 | `graal-build-time` hooks into the GraalVM `native-image` build process via a GraalVM `Feature` class. 51 | It inspects the classpath. 52 | Each class file that ends with `__init.class` is assumed to have been created by Clojure. 53 | The packages of these classes are then added to the list of packages to be initialized at build time. 54 | 55 | ## Overriding classes 56 | 57 | If there are classes in packages that you would like to initialize at runtime, you can 58 | use the `--initialize-at-run-time=my.org.MyClass` `native-image` argument. 59 | 60 | For example, when using http-kit, the `org.httpkit` package will be included for 61 | build-time initialization. You will need to override one class via `native-image` argument: 62 | ``` 63 | --initialize-at-run-time=org.httpkit.client.ClientSslEngineFactory$SSLHolder 64 | ``` 65 | 66 | ## Single segment namespaces 67 | 68 | This library doesn't work with single segment namespaces. 69 | A single segment namespace is one without any `.` characters in it, for example: `(ns digest)`. 70 | 71 | A single segment namespace, becomes, in the eyes of the JVM, package-less and ends up in the JVM default package. 72 | Single segment namespaces are problematic in general in Clojure and, because they are package-less, will not be initialized by `graal-build-time`. 73 | 74 | `graal-build-time` will emit a warning when it detects `.class` files generated from a single segment namespace, for example: 75 | 76 | ``` 77 | [clj-easy/graal-build-time] WARN: Single segment namespace found for class: digest__init.class. Because this class has no package, it cannot be registered for initialization at build time. 78 | ``` 79 | 80 | Starting with GraalVM v22, because it enables`--strict-image-heap` by default, you'll also see `native-image` fail your build when single segment namespaces are present. 81 | See [#35](https://github.com/clj-easy/graal-build-time/issues/35) for details if you are curious. 82 | 83 | ## Develop 84 | 85 | Run `bb tasks` for all relevant project tasks. 86 | 87 | Tasks attempt to avoid unnecessary work by comparing source and target file dates. 88 | If you want to skip this optimization, run `bb clean` before running your task. 89 | 90 | Use `bb native-image-test` to run our integration tests. 91 | - This task builds native images for a hello world app, and then runs them. 92 | - The hello world Clojure sources are compiled to Java classes. 93 | - We use GraalVM's `native-image` with `graal-build-time` on the classpath to create 2 variants of the same app: 94 | - one built from the uberjar 95 | - the other built directly from the classes dir 96 | - Note that we omit `--initialize-at-build-time` when creating the native images. 97 | The work that this deprecated option carried out is now taken care of by `graal-build-time`. 98 | - During native image creation, you'll see output that looks like: 99 | 100 | ``` 101 | [clj-easy/graal-build-time] Registering packages for build time initialization: clojure, clj_easy.graal_build_time, gbt_test_org, hello, hello_world 102 | ``` 103 | 104 | # License 105 | 106 | Licensed under the MIT license, see LICENSE. 107 | 108 | Copyright © 2021-2024 Michiel Borkent, Eric Dallo, Rahul Dé, Lee Read and contributors. 109 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:min-bb-version "1.3.182" 2 | :paths ["." "scripts"] 3 | :deps {lread/status-line {:git/url "https://github.com/lread/status-line.git" 4 | :sha "cf44c15f30ea3867227fa61ceb823e5e942c707f"}} 5 | :tasks 6 | {:requires ([babashka.fs :as fs] 7 | [babashka.tasks :as tasks] 8 | [babashka.process :refer [tokenize]] 9 | [build-shared :as bs] 10 | [clojure.string :as str]) 11 | :enter (when (not (str/starts-with? (:name (current-task)) "-")) 12 | (println (format "-[graal-build-time %s]----" (:name (current-task))))) 13 | :init 14 | (do 15 | (def windows? (str/starts-with? (System/getProperty "os.name") 16 | "Windows")) 17 | (defn clojure 18 | "Clojure defaulting to stderr output" 19 | [arg & args] 20 | (apply tasks/clojure (str "-J-Dclojure.main.report=stderr " arg) args))) 21 | 22 | clean {:doc "Clean target dir" 23 | :task (do 24 | (doseq [dir ["target" ".cpcache"]] 25 | (fs/delete-tree dir)) 26 | (doseq [dir ["test-hello-world" "test-single-segment"]] 27 | (shell {:dir dir} "bb clean")))} 28 | 29 | compile-sources {:doc "Compile sources" 30 | :task (if (seq (fs/modified-since bs/class-dir ["deps.edn" "src"])) 31 | (clojure "-T:build compile-sources") 32 | (println "Sources already compiled to" bs/class-dir))} 33 | 34 | jar {:doc "Build jar" 35 | :depends [compile-sources] 36 | :task (if (seq (fs/modified-since bs/jar-file [bs/class-dir])) 37 | (clojure "-T:build jar") 38 | (println "Jar is already up to date" bs/jar-file))} 39 | 40 | jar-name {:doc "Return jar name for test-hello-world build" 41 | :task (println bs/jar-file)} 42 | 43 | install {:doc "Install jar in local maven repo" 44 | :depends [jar] 45 | :task (clojure "-T:build install")} 46 | 47 | build-hello-world {:doc "Build jars, classes for testing" 48 | :task (shell {:dir "test-hello-world"} 49 | "bb build")} 50 | 51 | build-single-segment {:doc "Build jars, classes for single segment testing" 52 | :task (shell {:dir "test-single-segment"} 53 | "bb build")} 54 | 55 | test {:doc "Runs tests" 56 | :task (clojure "-X:test")} 57 | 58 | native-image-test {:doc "Runs native image hello world tests" 59 | :task (shell {:dir "test-hello-world"} 60 | "bb native-image-test")} 61 | 62 | lint {:doc "[--rebuild] Lint source code with clj-kondo" 63 | :task lint/-main} 64 | 65 | outdated {:doc "Report on outdated dependencies" 66 | :task (clojure "-M:outdated" (str "--directory=" (str/join ":" ["." 67 | "test-hello-world" 68 | "test-hello-world/lib1" 69 | "test-hello-world/lib2" 70 | "test-single-segment" 71 | "build-helper"])))} 72 | 73 | change-log-check {:doc "Checks that change log is ready for publish" 74 | :task (bs/change-log-check)} 75 | 76 | -change-log-update {:doc "Updates change log for current version" 77 | :depends [change-log-check] 78 | :task (bs/change-log-update)} 79 | 80 | -bump-version (do 81 | (bs/refresh-version) 82 | (println "Bumping current version" bs/version) 83 | (load-file "scripts/bump_version.clj") 84 | (bs/refresh-version) 85 | (println "Bumped version to" bs/version)) 86 | 87 | -commit-release (do 88 | (println "Committing changes made for release") 89 | (shell "git add resources/clj-easy/graal-build-time-version.txt CHANGELOG.md") 90 | (shell "git commit -m 'Bump version'")) 91 | 92 | -tag (do 93 | (println (str "Tagging with version v" bs/version)) 94 | (shell (str "git tag v" bs/version))) 95 | 96 | publish {:doc "Cut a new release" 97 | :task (do 98 | (run 'change-log-check) 99 | (run '-bump-version) 100 | (run '-change-log-update) 101 | (run '-commit-release) 102 | (run '-tag) 103 | (println "Pushing commit to github") 104 | (shell "git push --atomic origin main" 105 | (str "v" bs/version)))} 106 | 107 | -current-branch (->> (shell {:out :string} "git rev-parse --abbrev-ref HEAD") 108 | :out 109 | str/trim) 110 | 111 | -can-release {:depends [-current-branch] 112 | :task (= "main" -current-branch)} 113 | 114 | deploy {:doc "Deploys to clojars, automatically run on CI" 115 | :depends [-can-release] 116 | :task (if -can-release 117 | (do 118 | (run 'jar) 119 | (println "All set for deployment 🚀🚀") 120 | (tasks/clojure 121 | {:continue true} 122 | "-J-Dclojure.main.report=stderr -T:build deploy")) 123 | (println "Conditions for release did not pass, not deploying."))}}} 124 | -------------------------------------------------------------------------------- /build-helper/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {io.github.clojure/tools.build {:mvn/version "0.10.7"} 3 | babashka/fs {:mvn/version "0.5.24"}}} 4 | -------------------------------------------------------------------------------- /build-helper/src/build_helper/core.clj: -------------------------------------------------------------------------------- 1 | (ns build-helper.core 2 | "Reusable build support for internal test projects" 3 | (:require 4 | [clojure.string :as str] 5 | [clojure.tools.build.api :as b])) 6 | 7 | (defmacro with-err-str 8 | [& body] 9 | `(let [s# (new java.io.StringWriter)] 10 | (binding [*err* s#] 11 | ~@body 12 | (str s#)))) 13 | 14 | (defn uber [{:keys [lib main uber-file]}] 15 | (let [class-dir "target/classes" 16 | src-dirs ["src"] 17 | basis (b/create-basis)] 18 | (println "Writing pom") 19 | (->> (b/write-pom {:class-dir class-dir 20 | :lib lib 21 | :version "1.0.0" 22 | :basis basis 23 | :src-dirs ["src"]}) 24 | with-err-str 25 | str/split-lines 26 | ;; Avoid confusing future me/you: suppress "Skipping coordinate" messages for our jars, we don't care, we are creating an uberjar 27 | (remove #(re-matches #"^Skipping coordinate: \{:local/root .*target/(lib1|lib2|graal-build-time).jar.*" %)) 28 | (run! println)) 29 | (b/copy-dir {:src-dirs src-dirs 30 | :target-dir class-dir}) 31 | (println "Compile sources to classes") 32 | (b/compile-clj {:basis basis 33 | :src-dirs src-dirs 34 | :class-dir class-dir 35 | :ns-compile [main]}) 36 | (println "Building uberjar" uber-file) 37 | (b/uber {:class-dir class-dir 38 | :uber-file uber-file 39 | :basis basis 40 | :main main}))) 41 | 42 | (defn jar [{:keys [lib jar-file]}] 43 | (let [class-dir "target/classes" 44 | src-dirs ["src"] 45 | basis (b/create-basis)] 46 | (println "Writing pom") 47 | (b/write-pom {:class-dir class-dir 48 | :lib lib 49 | :version "1.0.0" 50 | :basis basis 51 | :src-dirs ["src"]}) 52 | (b/copy-dir {:src-dirs src-dirs 53 | :target-dir class-dir}) 54 | (println "Compile sources to classes") 55 | (b/compile-clj {:basis basis 56 | :src-dirs src-dirs 57 | :class-dir class-dir}) 58 | (println "Building jar" jar-file) 59 | (b/jar {:class-dir class-dir 60 | :jar-file jar-file 61 | :basis basis}))) 62 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:refer-clojure :exclude [compile]) 3 | (:require 4 | [build-shared :as bs :refer [lib]] 5 | [clojure.tools.build.api :as b] 6 | [deps-deploy.deps-deploy :as dd])) 7 | 8 | (def class-dir bs/class-dir) 9 | (def basis (b/create-basis {:project "deps.edn"})) 10 | (def with-svm-basis (b/create-basis {:project "deps.edn" 11 | :aliases [:svm]})) 12 | (def version bs/version) 13 | (def jar-file bs/jar-file) 14 | 15 | (defn compile-sources [{:keys [class-dir] :or {class-dir class-dir}}] 16 | (println "Compiling Clojure sources to:" class-dir) 17 | (b/compile-clj {:basis basis 18 | :src-dirs bs/sources 19 | :class-dir class-dir}) 20 | (println "Done compiling Clojure sources.") 21 | (println "Compiling java sources.") 22 | (b/javac {:src-dirs bs/sources 23 | :class-dir class-dir 24 | :basis with-svm-basis 25 | :javac-opts ["--release" "8" ;; technically current Graal jdk version is our current jdk min, 26 | ;; but clojure produces jdk 8 compatible classes, so we'll arbitrarily 27 | ;; match that 28 | ;; --release was introduce after jdk8, so we'll fail 29 | ;; if compiling with <= jdk8, which is fine. 30 | "-Xlint:-options" "-Werror"]}) 31 | (println "Done compiling java sources.")) 32 | 33 | (defn jar [_] 34 | (println "Producing jar:" jar-file) 35 | (let [gh-coords "github.com/clj-easy/graal-build-time" 36 | project-url (str "https://" gh-coords)] 37 | (b/write-pom {:class-dir class-dir 38 | :lib lib 39 | :version version 40 | :scm {:connection (format "scm:git:git://%s.git" gh-coords) 41 | :developerConnection (format "scm:git:ssh://git@%s.git" gh-coords) 42 | :tag (format "v%s" version) 43 | :url project-url} 44 | :basis basis 45 | :src-dirs ["src"] 46 | :pom-data [[:description "Initialize Clojure classes at build time with GraalVM native-image"] 47 | [:url project-url] 48 | [:licenses 49 | [:license 50 | [:name "The MIT License"] 51 | [:url "https://opensource.org/licenses/MIT"]]] 52 | [:properties 53 | [:project.build.sourceEncoding "UTF-8"]]]})) 54 | (b/copy-dir {:src-dirs ["src" "resources"] 55 | :target-dir class-dir}) 56 | (b/jar {:class-dir class-dir 57 | :jar-file jar-file}) 58 | (println "Done building jar.")) 59 | 60 | (defn install 61 | [_] 62 | (b/install {:basis basis 63 | :lib lib 64 | :version version 65 | :jar-file jar-file 66 | :class-dir class-dir}) 67 | (println "Installed" lib version "in local maven repo.")) 68 | 69 | (defn deploy [opts] 70 | (println "Deploying version" jar-file "to Clojars.") 71 | (dd/deploy (merge {:installer :remote 72 | :artifact jar-file 73 | :pom-file (b/pom-path {:lib lib :class-dir class-dir})} 74 | opts)) 75 | opts) 76 | -------------------------------------------------------------------------------- /build_shared.clj: -------------------------------------------------------------------------------- 1 | (ns build-shared 2 | (:require [clojure.string :as str])) 3 | 4 | (def version-file "resources/clj-easy/graal-build-time-version.txt") 5 | (def version nil) 6 | (defn refresh-version [] 7 | (alter-var-root #'version (constantly (str/trim (slurp version-file))))) 8 | (refresh-version) 9 | 10 | (def target "target") 11 | (def lib 'com.github.clj-easy/graal-build-time) 12 | (def jar-file (format "%s/%s-%s.jar" target (name lib) version)) 13 | (def sources ["src"]) 14 | (def class-dir (str target "/classes")) 15 | 16 | (defn change-log-check [] 17 | (let [log (slurp "CHANGELOG.md") 18 | new-changes (last (re-find #"(?ms)^## Unreleased *$(.*?)^##|\z" log))] 19 | (if (not (and new-changes (re-find #"(?ims)[a-z]" new-changes))) 20 | (do 21 | (println "FAIL: Change log must contain Unreleased section with some text.") 22 | (System/exit 1)) 23 | (println "PASS: Change log Unreleased section found with some text.")))) 24 | 25 | (defn change-log-update [] 26 | (println "Updating change log") 27 | (refresh-version) 28 | (let [log (slurp "CHANGELOG.md") 29 | new-log (str/replace-first 30 | log 31 | #"(?ms)^## Unreleased *$" 32 | (str "## Unreleased\n\n-\n\n## v" version))] 33 | (spit "CHANGELOG.md" new-log) 34 | (println (str "Change log updated for v" version)))) 35 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.12.0"}} 2 | :paths ["src" "resources"] 3 | :aliases 4 | {:svm 5 | ;; this library is "provided" 6 | {:extra-deps {org.graalvm.sdk/nativeimage {:mvn/version "24.2.0"}}} 7 | :build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.7"} 8 | babashka/fs {:mvn/version "0.5.24"} 9 | babashka/process {:mvn/version "0.5.22"} 10 | slipset/deps-deploy {:mvn/version "0.2.2"}} 11 | :ns-default build} 12 | :uber {:extra-paths ["test"]} 13 | :clj-kondo {:extra-deps {clj-kondo/clj-kondo {:mvn/version "2025.02.20"}} 14 | :main-opts ["-m" "clj-kondo.main"]} 15 | :test {:extra-paths ["test"] 16 | :extra-deps {io.github.cognitect-labs/test-runner 17 | {:git/tag "v0.5.1" :git/sha "dfb30dd"} 18 | babashka/process {:mvn/version "0.5.22"}} 19 | :exec-fn cognitect.test-runner.api/test} 20 | :outdated {:deps {org.slf4j/slf4j-simple {:mvn/version "2.0.17"} ;; to rid ourselves of logger warnings 21 | com.github.liquidz/antq {:mvn/version "2.11.1276"}} 22 | :main-opts ["-m" "antq.core"]}}} 23 | -------------------------------------------------------------------------------- /resources/clj-easy/graal-build-time-version.txt: -------------------------------------------------------------------------------- 1 | 1.0.5 -------------------------------------------------------------------------------- /scripts/bump_version.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (ns bump-version 4 | (:require [clojure.java.io :as io] 5 | [clojure.string :as str])) 6 | 7 | (def version-file (io/file "resources" "clj-easy" "graal-build-time-version.txt")) 8 | 9 | (case (first *command-line-args*) 10 | (let [version-string (str/trim (slurp version-file)) 11 | [major minor patch] (str/split version-string #"\.") 12 | new-version (str/join "." [major minor (inc (Integer/parseInt patch))])] 13 | (spit version-file new-version))) 14 | -------------------------------------------------------------------------------- /scripts/lint.clj: -------------------------------------------------------------------------------- 1 | (ns lint 2 | (:require [babashka.classpath :as bbcp] 3 | [babashka.cli :as cli] 4 | [babashka.fs :as fs] 5 | [babashka.tasks :as t] 6 | [clojure.string :as string] 7 | [lread.status-line :as status])) 8 | 9 | (def clj-kondo-cache ".clj-kondo/.cache") 10 | 11 | (defn- cache-exists? [] 12 | (fs/exists? clj-kondo-cache)) 13 | 14 | (defn- delete-cache [] 15 | (when (cache-exists?) 16 | (fs/delete-tree clj-kondo-cache))) 17 | 18 | (defn- build-cache [] 19 | (when (cache-exists?) 20 | (delete-cache)) 21 | (let [clj-cp (-> (t/clojure {:out :string} 22 | "-Spath -M:test:build") 23 | with-out-str 24 | string/trim) 25 | bb-cp (bbcp/get-classpath)] 26 | (status/line :detail "- copying lib configs and creating cache") 27 | (t/clojure "-M:clj-kondo --skip-lint --copy-configs --dependencies --parallel --lint" clj-cp bb-cp))) 28 | 29 | (defn- check-cache [{:keys [rebuild]}] 30 | (status/line :head "clj-kondo: cache check") 31 | (if-let [rebuild-reason (cond 32 | rebuild 33 | "Rebuild requested" 34 | 35 | (not (cache-exists?)) 36 | "Cache not found" 37 | 38 | :else 39 | (let [updated-dep-files (fs/modified-since clj-kondo-cache ["deps.edn" "bb.edn"])] 40 | (when (seq updated-dep-files) 41 | (format "Found deps files newer than lint cache: %s" (mapv str updated-dep-files)))))] 42 | (do (status/line :detail rebuild-reason) 43 | (build-cache)) 44 | (status/line :detail "Using existing cache"))) 45 | 46 | (defn- lint [opts] 47 | (check-cache opts) 48 | (status/line :head "clj-kondo: linting") 49 | (let [{:keys [exit]} 50 | (t/clojure {:continue true} 51 | "-M:clj-kondo --parallel --lint src test scripts build.clj build_shared.clj deps.edn bb.edn")] 52 | (cond 53 | (= 2 exit) (status/die exit "clj-kondo found one or more lint errors") 54 | (= 3 exit) (status/die exit "clj-kondo found one or more lint warnings") 55 | (> exit 0) (status/die exit "clj-kondo returned unexpected exit code")))) 56 | 57 | (defn -main [& args] 58 | (when-let [opts (cli/parse-opts args)] 59 | (lint opts))) 60 | 61 | (when (= *file* (System/getProperty "babashka.file")) 62 | (apply -main *command-line-args*)) 63 | -------------------------------------------------------------------------------- /src/clj_easy/graal_build_time/InitClojureClasses.java: -------------------------------------------------------------------------------- 1 | package clj_easy.graal_build_time; 2 | 3 | import java.util.List; 4 | import java.nio.file.Path; 5 | 6 | import org.graalvm.nativeimage.hosted.Feature; 7 | import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; 8 | 9 | public class InitClojureClasses implements Feature { 10 | 11 | @Override 12 | public void duringSetup(DuringSetupAccess access) { 13 | List classPath = access.getApplicationClassPath(); 14 | String[] packages = clj_easy.graal_build_time.packages.list(classPath); 15 | String packagesStr = clj_easy.graal_build_time.packages.listStr(packages); 16 | System.out.println("[clj-easy/graal-build-time] Registering packages for build time initialization: " + packagesStr); 17 | RuntimeClassInitialization.initializeAtBuildTime(packages); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/clj_easy/graal_build_time/packages.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc clj-easy.graal-build-time.packages 2 | (:require [clojure.string :as str]) 3 | (:import [java.nio.file Path] 4 | [java.util.jar JarFile JarFile$JarFileEntry]) 5 | (:gen-class 6 | :methods [^{:static true} [list [java.util.List] "[Ljava.lang.String;"] 7 | ^{:static true} [listStr ["[Ljava.lang.String;"] String]])) 8 | 9 | (def ^:private jar-entry-file-separator "/") 10 | 11 | (defn ^:private entry->package [nm split] 12 | (let [package (->> (str/split nm (re-pattern (str/re-quote-replacement split))) 13 | drop-last 14 | (str/join "."))] 15 | (when (str/blank? package) 16 | (println (str "[clj-easy/graal-build-time] WARN: Single segment namespace found for class: " nm ". " 17 | "Because this class has no package, it cannot be registered for initialization at build time."))) 18 | package)) 19 | 20 | (defn ^:private consider-entry? [nm file-sep] 21 | (and (not (str/starts-with? nm (str "clojure" file-sep))) 22 | (str/ends-with? nm "__init.class"))) 23 | 24 | (defn ^:private contains-parent? [packages package] 25 | (some #(and (not= % package) 26 | (str/starts-with? (str package ".") (str % "."))) 27 | packages)) 28 | 29 | (defn ^:private unique-packages [packages] 30 | (->> packages 31 | (remove (partial contains-parent? packages)) 32 | set)) 33 | 34 | (defn ^:private packages-from-jar 35 | [^Path jar-file] 36 | (with-open [jar (JarFile. (.toFile jar-file))] 37 | (let [entries (enumeration-seq (.entries jar)) 38 | packages (->> entries 39 | (map #(.getName ^JarFile$JarFileEntry %)) 40 | (filter #(consider-entry? % jar-entry-file-separator)) 41 | (map #(entry->package % jar-entry-file-separator)) 42 | (remove str/blank?) 43 | vec)] 44 | packages))) 45 | 46 | (defn ^:private packages-from-dir [^Path dir] 47 | (let [f (.toFile dir) 48 | files (rest (file-seq f)) 49 | relatives (map (fn [^java.io.File f] 50 | (let [path (.toPath f)] 51 | (.relativize dir path))) 52 | files) 53 | names (map str relatives) 54 | packages (->> names 55 | (filter #(consider-entry? % (System/getProperty "file.separator"))) 56 | (map #(entry->package % (System/getProperty "file.separator"))) 57 | (remove str/blank?))] 58 | packages)) 59 | 60 | (defn -list [paths] 61 | (->> paths 62 | (mapcat (fn [path] 63 | (if (str/ends-with? (str path) ".jar") 64 | (packages-from-jar path) 65 | (packages-from-dir path)))) 66 | unique-packages 67 | sort 68 | (cons "clojure") 69 | distinct 70 | into-array)) 71 | 72 | (defn -listStr [pl] 73 | (str/join ", " pl)) 74 | -------------------------------------------------------------------------------- /test-hello-world/bb.edn: -------------------------------------------------------------------------------- 1 | {:tasks 2 | {:requires ([babashka.fs :as fs] 3 | [babashka.tasks :as tasks] 4 | [clojure.string :as str]) 5 | :enter (when (not (str/starts-with? (:name (current-task)) "-")) 6 | (println (format "-[hello-world %s]----" (:name (current-task))))) 7 | :init (do 8 | (defn clojure 9 | "Clojure defaulting to stderr output" 10 | [arg & args] 11 | (apply tasks/clojure (str "-J-Dclojure.main.report=stderr " arg) args))) 12 | 13 | -test-libs ["lib1" "lib2"] 14 | -artifact "target/hello-world.jar" 15 | -native-image-path "target" 16 | 17 | clean 18 | {:depends [-test-libs] 19 | :task (do 20 | (doseq [dir ["target" ".cpcache"]] 21 | (fs/delete-tree dir)) 22 | (doseq [p -test-libs] 23 | (shell {:dir p} "bb clean")))} 24 | 25 | libs 26 | {:doc "Build test libs" 27 | :depends [-test-libs] 28 | :task (doseq [p -test-libs] 29 | (shell {:dir p} "bb build"))} 30 | 31 | graal-build-time-jar 32 | {:doc "Build and grab a copy of local graal build time jar from above." 33 | :task (do 34 | (shell {:dir ".."} "bb jar") 35 | (let [jar-name (-> (shell {:out :string :dir ".."} 36 | "bb jar-name") 37 | :out 38 | str/split-lines 39 | last)] 40 | (println "copying" jar-name) 41 | (fs/create-dirs "target") 42 | (fs/copy (str "../" jar-name) 43 | "target/graal-build-time.jar" 44 | {:replace-existing true})))} 45 | 46 | build {:doc "Build classes and uberjar" 47 | :depends [libs 48 | -artifact 49 | graal-build-time-jar] 50 | :task 51 | (if (seq (fs/modified-since -artifact ["../lib1/target/lib1.jar" 52 | "../lib2/target/lib2.jar" 53 | "src" 54 | "bb.edn" 55 | "deps.edn" 56 | "build.clj"])) 57 | (do 58 | (println "Building" -artifact) 59 | (clojure "-T:build uber")) 60 | (println -artifact "already built"))} 61 | 62 | graalvm {:doc "Checks GRAALVM_HOME env var" 63 | :task 64 | (let [env (System/getenv "GRAALVM_HOME")] 65 | (assert env "Set GRAALVM_HOME") 66 | env)} 67 | 68 | -graalvm-native-image-exe 69 | {:doc "Resolves and returns graalvm native-image binary" 70 | :task (when-let [native-image (if-let [ghome (System/getenv "GRAALVM_HOME")] 71 | (or (fs/which (fs/file ghome "bin" "native-image")) 72 | (throw (ex-info "Could not find GraalVM native-image via GRAALVM_HOME." {}))) 73 | (or (fs/which "native-image") 74 | (throw (ex-info "GRAALVM_HOME not set, and did not find GraalVM native-image on PATH" {}))))] 75 | (println "Using GraalVM native-image:" (str native-image)) 76 | (shell native-image "--version") 77 | native-image)} 78 | 79 | ;; 80 | ;; native image from uber 81 | ;; 82 | -native-image-uber-name 83 | "hello-world-uber" 84 | 85 | native-image-uber 86 | {:doc "Builds native image from uber" 87 | :depends [build 88 | -graalvm-native-image-exe 89 | -native-image-path 90 | -native-image-uber-name] 91 | :task (let [exe (str (fs/file -native-image-path -native-image-uber-name)) 92 | full-exe (str (fs/which exe))] 93 | (if (seq (fs/modified-since full-exe ["target/hello-world.jar"])) 94 | (do (println "Building" exe) 95 | (shell -graalvm-native-image-exe 96 | ;; note: we are omitting --initialize-at-build-time 97 | "-jar" "target/hello-world.jar" 98 | "--features=clj_easy.graal_build_time.InitClojureClasses" 99 | "-O1" ;; basic optimization for faster build 100 | "--no-fallback" 101 | "--verbose" 102 | "-o" exe)) 103 | (println "Already built" full-exe)))} 104 | 105 | native-image-uber-test 106 | {:doc "Run native image built from uber" 107 | :depends [native-image-uber 108 | -native-image-path 109 | -native-image-uber-name] 110 | :task (let [full-exe (str (fs/which (fs/file -native-image-path -native-image-uber-name)))] 111 | (println "Running" full-exe) 112 | (shell full-exe))} 113 | 114 | ;; 115 | ;; native image from classes 116 | ;; 117 | -native-image-classes-name 118 | "hello-world-classes" 119 | 120 | native-image-classes 121 | {:doc "Builds native image from classes" 122 | :depends [build 123 | -graalvm-native-image-exe 124 | -native-image-path 125 | -native-image-classes-name] 126 | :task 127 | (let [exe (str (fs/file -native-image-path -native-image-classes-name)) 128 | full-exe (str (fs/which exe))] 129 | (if (seq (fs/modified-since exe "target/classes")) 130 | (do (println "Building" exe) 131 | (shell -graalvm-native-image-exe 132 | ;; note: we are omitting --initialize-at-build-time 133 | "-cp" (str "target/classes" 134 | (System/getProperty "path.separator") 135 | (-> (with-out-str (clojure "-Spath")) str/trim)) 136 | "--features=clj_easy.graal_build_time.InitClojureClasses" 137 | "-O1" ;; basic optimization for faster build 138 | "--no-fallback" 139 | "--verbose" 140 | "-o" exe 141 | "hello_world.main"))) 142 | (println "Already built" full-exe))} 143 | 144 | native-image-classes-test 145 | {:doc "Run native image built from classes" 146 | :depends [native-image-classes 147 | -native-image-path 148 | -native-image-classes-name] 149 | :task (let [full-exe (str (fs/which (fs/file -native-image-path -native-image-classes-name)))] 150 | (println "Running" full-exe) 151 | (shell full-exe))} 152 | 153 | ;; 154 | ;; run all native image tests 155 | ;; 156 | native-image-test 157 | {:doc "Run integration tests" 158 | :depends [native-image-uber-test native-image-classes-test] 159 | :task (println "Done.")}}} 160 | -------------------------------------------------------------------------------- /test-hello-world/build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require [build-helper.core :as bh])) 3 | 4 | (defn uber [_] 5 | (bh/uber {:lib 'hello-world/hello-world 6 | :main 'hello-world.main 7 | :uber-file "target/hello-world.jar"})) 8 | -------------------------------------------------------------------------------- /test-hello-world/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0"} 3 | lib1/lib1 {:local/root "lib1/target/lib1.jar"} 4 | lib2/lib2 {:local/root "lib2/target/lib2.jar"} 5 | clj-easy/graal-build-time {:local/root "target/graal-build-time.jar"}} 6 | :aliases {:build {:deps {clj-easy/build-helper {:local/root "../build-helper"}} 7 | :ns-default build}}} 8 | -------------------------------------------------------------------------------- /test-hello-world/lib1/bb.edn: -------------------------------------------------------------------------------- 1 | {:tasks 2 | {:requires ([babashka.fs :as fs] 3 | [clojure.string :as str]) 4 | :enter (when (not (str/starts-with? (:name (current-task)) "-")) 5 | (println (format "-[lib1 %s]----" (:name (current-task))))) 6 | 7 | clean (doseq [dir ["target" ".cpcache"]] 8 | (fs/delete-tree dir)) 9 | -artifact "target/lib1.jar" 10 | build {:depends [-artifact] 11 | :task 12 | (if (seq (fs/modified-since -artifact ["src" "bb.edn" "deps.edn" "build.clj"])) 13 | (do (println "Building" -artifact) 14 | (clojure "-T:build jar")) 15 | (println -artifact "already built"))}}} 16 | -------------------------------------------------------------------------------- /test-hello-world/lib1/build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require [build-helper.core :as bh])) 3 | 4 | (defn jar [_] 5 | (bh/jar {:lib 'gbt-test-org/lib1 6 | :jar-file "target/lib1.jar"})) 7 | -------------------------------------------------------------------------------- /test-hello-world/lib1/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :aliases {:build {:deps {clj-easy/build-helper {:local/root "../../build-helper"}} 3 | :ns-default build}}} 4 | -------------------------------------------------------------------------------- /test-hello-world/lib1/src/gbt_test_org/p2/core.clj: -------------------------------------------------------------------------------- 1 | (ns gbt-test-org.p2.core 2 | "This package is a child package of a parent package that occurs in lib2. 3 | It should not therefore not be included in build init registration.") 4 | 5 | (defn dummy []) 6 | -------------------------------------------------------------------------------- /test-hello-world/lib2/bb.edn: -------------------------------------------------------------------------------- 1 | {:tasks 2 | {:requires ([babashka.fs :as fs] 3 | [clojure.string :as str]) 4 | :enter (when (not (str/starts-with? (:name (current-task)) "-")) 5 | (println (format "-[lib2 %s]----" (:name (current-task))))) 6 | 7 | clean (doseq [dir ["target" ".cpcache"]] 8 | (fs/delete-tree dir)) 9 | -artifact "target/lib2.jar" 10 | build {:depends [-artifact] 11 | :task 12 | (if (seq (fs/modified-since -artifact ["src" "bb.edn" "deps.edn" "build.clj"])) 13 | (do (println "Building" -artifact) 14 | (clojure "-T:build jar")) 15 | (println -artifact "already built"))}}} 16 | -------------------------------------------------------------------------------- /test-hello-world/lib2/build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require [build-helper.core :as bh])) 3 | 4 | (defn jar [_] 5 | (bh/jar {:lib 'gbt-test-org/lib2 6 | :jar-file "target/lib2.jar"})) 7 | -------------------------------------------------------------------------------- /test-hello-world/lib2/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :aliases {:build {:deps {clj-easy/build-helper {:local/root "../../build-helper"}} 3 | :ns-default build}}} 4 | -------------------------------------------------------------------------------- /test-hello-world/lib2/src/gbt_test_org/core.clj: -------------------------------------------------------------------------------- 1 | (ns gbt-test-org.core 2 | "This package should be included in build init registration.") 3 | 4 | (defn dummy []) 5 | -------------------------------------------------------------------------------- /test-hello-world/src/hello/core.clj: -------------------------------------------------------------------------------- 1 | (ns hello.core 2 | "This package will be included in build init registration. 3 | It is included to test that: 4 | hello_world is not excluded because it happens to start with this package's name: 5 | hello") 6 | 7 | (defn dummy []) 8 | -------------------------------------------------------------------------------- /test-hello-world/src/hello_world/main.clj: -------------------------------------------------------------------------------- 1 | (ns hello-world.main 2 | (:require [hello.core] 3 | [gbt-test-org.core] 4 | [gbt-test-org.p2.core]) 5 | (:gen-class)) 6 | 7 | (defn -main [] 8 | ;; sanity for test nses loaded and included 9 | (hello.core/dummy) 10 | (gbt-test-org.core/dummy) 11 | (gbt-test-org.p2.core/dummy) 12 | (println "Hello world")) 13 | -------------------------------------------------------------------------------- /test-single-segment/bb.edn: -------------------------------------------------------------------------------- 1 | {:tasks 2 | {:requires ([babashka.fs :as fs] 3 | [babashka.tasks :as tasks] 4 | [clojure.string :as str]) 5 | :enter (when (not (str/starts-with? (:name (current-task)) "-")) 6 | (println (format "-[test-single-segment %s]----" (:name (current-task))))) 7 | :init (do 8 | (defn clojure 9 | "Clojure defaulting to stderr output" 10 | [arg & args] 11 | (apply tasks/clojure (str "-J-Dclojure.main.report=stderr " arg) args))) 12 | 13 | -artifact "target/single-segment.jar" 14 | 15 | clean 16 | {:task (doseq [dir ["target" ".cpcache"]] 17 | (fs/delete-tree dir))} 18 | 19 | graal-build-time-jar 20 | {:doc "Build and grab a copy of local graal build time jar from above." 21 | :task (do 22 | (shell {:dir ".."} "bb jar") 23 | (let [jar-name (-> (shell {:out :string :dir ".."} 24 | "bb jar-name") 25 | :out 26 | str/split-lines 27 | last)] 28 | (println "copying" jar-name) 29 | (fs/create-dirs "target") 30 | (fs/copy (str "../" jar-name) 31 | "target/graal-build-time.jar" 32 | {:replace-existing true})))} 33 | 34 | build {:doc "Build classes and uberjar" 35 | :depends [-artifact 36 | graal-build-time-jar] 37 | :task 38 | (if (seq (fs/modified-since -artifact ["src" 39 | "bb.edn" 40 | "deps.edn" 41 | "build.clj"])) 42 | (do 43 | (println "Building" -artifact) 44 | (clojure "-T:build uber")) 45 | (println -artifact "already built"))}}} 46 | -------------------------------------------------------------------------------- /test-single-segment/build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require [build-helper.core :as bh])) 3 | 4 | (defn uber [_] 5 | (bh/uber {:lib 'single-segment/single-segment 6 | :main 'single-segment.main 7 | :uber-file "target/single-segment.jar"})) 8 | -------------------------------------------------------------------------------- /test-single-segment/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.12.0"} 2 | org.clj-commons/digest {:mvn/version "1.4.100"} 3 | clj-easy/graal-build-time {:local/root "target/graal-build-time.jar"}} 4 | :aliases {:build {:deps {clj-easy/build-helper {:local/root "../build-helper"}} 5 | :ns-default build}}} 6 | -------------------------------------------------------------------------------- /test-single-segment/src/single_segment/main.clj: -------------------------------------------------------------------------------- 1 | (ns single-segment.main 2 | "Exists to test our emitting of warning message for single segement namespaces" 3 | (:require [digest]) ;; this deprecated ns is, as you can see, single segment 4 | (:gen-class)) 5 | 6 | (defn -main [] 7 | (println "md5 digest for clojure" (digest/md5 "clojure"))) 8 | -------------------------------------------------------------------------------- /test/clj_easy/graal_build_time/packages_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-easy.graal-build-time.packages-test 2 | (:require [babashka.process :as p] 3 | [clj-easy.graal-build-time.packages :as packages] 4 | [clojure.java.io :as io] 5 | [clojure.test :refer [deftest is testing]])) 6 | 7 | (deftest hello-world-package-list-test 8 | (-> (p/process ["bb" "build-hello-world"] {:inherit true}) 9 | (p/check)) 10 | (let [expected-packages "clojure, clj_easy.graal_build_time, gbt_test_org, hello, hello_world"] 11 | (testing "packages from directory" 12 | (is (= expected-packages 13 | (-> (packages/-list (->> ["test-hello-world/target/classes" 14 | "test-hello-world/lib1/target/lib1.jar" 15 | "test-hello-world/lib2/target/lib2.jar" 16 | "test-hello-world/target/graal-build-time.jar"] 17 | (mapv #(.toPath (io/file %))))) 18 | (packages/-listStr))))) 19 | (testing "packages from jar" 20 | (is (= expected-packages 21 | (-> (packages/-list [(.toPath (io/file "test-hello-world/target/hello-world.jar"))]) 22 | (packages/-listStr))))))) 23 | 24 | (deftest single-segment-warning-message-test 25 | (-> (p/process ["bb" "build-single-segment"] {:inherit true}) 26 | (p/check)) 27 | (let [expected-packages "clojure, clj_easy.graal_build_time, single_segment" 28 | expected-warning #"^\[clj-easy/graal-build-time\] WARN: Single segment .* digest__init.class"] 29 | (testing "packages from directory" 30 | (is (re-find 31 | expected-warning 32 | (with-out-str 33 | (is (= expected-packages 34 | (-> (packages/-list (->> ["test-single-segment/target/classes" 35 | "test-single-segment/target/graal-build-time.jar"] 36 | (mapv #(.toPath (io/file %))))) 37 | (packages/-listStr)))))))) 38 | (testing "packages from jar" 39 | (is (re-find 40 | expected-warning 41 | (with-out-str 42 | (is (= expected-packages 43 | (-> (packages/-list [(.toPath (io/file "test-single-segment/target/single-segment.jar"))]) 44 | (packages/-listStr)))))))))) 45 | --------------------------------------------------------------------------------