├── .VERSION_PREFIX ├── .dir-locals.el ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── bb.edn ├── bin └── proj ├── deps.edn ├── pom.xml ├── repl_session └── poke.clj └── src ├── .gitkeep └── lambdaisland ├── classpath.clj └── classpath └── watch_deps.clj /.VERSION_PREFIX: -------------------------------------------------------------------------------- 1 | 0.6 -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((cider-clojure-cli-global-options . "-A:dev:test")))) 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | .nrepl-port 3 | target 4 | repl 5 | scratch.clj 6 | .shadow-cljs 7 | target 8 | yarn.lock 9 | node_modules/ 10 | .DS_Store 11 | resources/public/ui 12 | .store 13 | out 14 | .#* 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | ## Added 4 | 5 | ## Fixed 6 | 7 | ## Changed 8 | 9 | # 0.6.58 (2024-12-04 / 3edd4ca) 10 | 11 | ## Fixed 12 | 13 | - Fix handling of InnocuousThread (i.e. skip them, not much more we can do) 14 | 15 | ## Changed 16 | 17 | - Upgraded dependencies 18 | 19 | # 0.5.48 (2024-01-10 / 2228a54) 20 | 21 | ## Changed 22 | 23 | - Upgraded dependencies 24 | 25 | # 0.4.44 (2022-09-08 / 0c66dda) 26 | 27 | ## Added 28 | 29 | - [watcher] Add a `:watch-paths` option, to watch additional files. Presumable 30 | in combination with a custom `:basis-fn` 31 | 32 | # 0.3.40 (2022-09-08 / 73c9529) 33 | 34 | ## Added 35 | 36 | - [watcher] Support for custom basis-fn 37 | - [watcher] Check for aliases in `:extra` deps file 38 | 39 | ## Fixed 40 | 41 | - [watcher] Fix watcher stop! function 42 | 43 | # 0.2.37 (2022-08-26 / 34be62f) 44 | 45 | ## Changed 46 | 47 | - Upgrade directory-watcher to the latest version 48 | - Prefix output from watch-deps with `[watch-deps]` 49 | - Print a message when the watcher triggers, but no classpath changes are made 50 | 51 | # 0.1.33 (2022-08-25 / fd51db4) 52 | 53 | - Fix typo in the README's example and one in doc-string 54 | 55 | ## Added 56 | 57 | - Support watching multiple `deps.edn` files referenced via `:local/root` 58 | 59 | ## Fixed 60 | 61 | ## Changed 62 | 63 | # 0.0.27 (2021-10-06 / 719c1f5) 64 | 65 | ## Fixed 66 | 67 | - Watch-deps now triggers on Mac 68 | - Support both `main` and `master` as branch names in `git-pull-lib` 69 | - Speed up resource lookups in priority classloader 70 | - Several extra convenience functions for working with classloaders 71 | 72 | # 0.0.0 73 | 74 | ## First announced version (git only) 75 | 76 | - Classpath inspection utilities 77 | - Priority classloader for overrides 78 | - deps watcher 79 | - git-pull-lib -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambdaisland/classpath 2 | 3 | 4 | [![cljdoc badge](https://cljdoc.org/badge/com.lambdaisland/classpath)](https://cljdoc.org/d/com.lambdaisland/classpath) [![Clojars Project](https://img.shields.io/clojars/v/com.lambdaisland/classpath.svg)](https://clojars.org/com.lambdaisland/classpath) 5 | 6 | 7 | Experimental utilities for dealing with "the classpath", and dynamically loading libraries. 8 | 9 | Blog post: [The Classpath is a Lie](https://lambdaisland.com/blog/2021-08-25-classpath-is-a-lie) 10 | 11 | ## With thanks to Nextjournal! 12 | 13 | [Nextjournal](https://nextjournal.com/) provided the incentive and financial 14 | support to dive into this. Many thanks to them for pushing to move the needle on 15 | dev experience. 16 | 17 | ## Usage 18 | 19 | ### Watch `deps.edn` 20 | 21 | ```clojure 22 | (require '[lambdaisland.classpath.watch-deps :as watch-deps]) 23 | 24 | (watch-deps/start! {:aliases [:dev :test]}) 25 | ``` 26 | 27 | Whenever you change `deps.edn` this will pick up any extra libraries or changed 28 | versions, and add them to the classpath. 29 | 30 | Caveat: we can only *add* to the classpath, any dependencies that were present 31 | when the app started will remain accessible. 32 | 33 | You can pass the option `:include-local-roots? true` to also watch any 34 | `deps.edn` of projects that are referenced via `:local/root` in your project's 35 | `deps.edn` 36 | 37 | ### Classpath inspection and manipulation 38 | 39 | ```clojure 40 | (require '[lambdaisland.classpath :as licp]) 41 | ``` 42 | 43 | Get the current chain of classloaders 44 | 45 | ```clojure 46 | (licp/classloader-chain) 47 | ``` 48 | 49 | Also see which entries each loader searches 50 | 51 | ```clojure 52 | (licp/classpath-chain) 53 | ``` 54 | 55 | Update a gitlib in `deps.edn` to the latest `:git/sha` in `main` or in the specified `:git/branch` 56 | 57 | ```clojure 58 | (licp/git-pull-lib 'com.lambdaisland/ornament) 59 | ``` 60 | 61 | Add/override the classpath based on the current deps.edn. 62 | 63 | ```clojure 64 | (licp/update-classpath! 65 | '{:aliases [:dev :test :licp] 66 | :extra {:deps {com.lambdaisland/webstuff {:local/root "/home/arne/github/lambdaisland/webstuff"}}}}) 67 | ``` 68 | 69 | Access specific class loaders 70 | 71 | ```clojure 72 | (licp/context-classloader) 73 | (licp/base-loader) 74 | (licp/root-loader) 75 | (licp/compiler-loader) 76 | ``` 77 | 78 | 79 | ## Lambda Island Open Source 80 | 81 | 82 | 83 |   84 | 85 | classpath is part of a growing collection of quality Clojure libraries created and maintained 86 | by the fine folks at [Gaiwan](https://gaiwan.co). 87 | 88 | Pay it forward by [becoming a backer on our Open Collective](http://opencollective.com/lambda-island), 89 | so that we may continue to enjoy a thriving Clojure ecosystem. 90 | 91 | You can find an overview of our projects at [lambdaisland/open-source](https://github.com/lambdaisland/open-source). 92 | 93 |   94 | 95 |   96 | 97 | 98 | 99 | ## Contributing 100 | 101 | Everyone has a right to submit patches to classpath, and thus become a contributor. 102 | 103 | Contributors MUST 104 | 105 | - adhere to the [LambdaIsland Clojure Style Guide](https://nextjournal.com/lambdaisland/clojure-style-guide) 106 | - write patches that solve a problem. Start by stating the problem, then supply a minimal solution. `*` 107 | - agree to license their contributions as MPL 2.0. 108 | - not break the contract with downstream consumers. `**` 109 | - not break the tests. 110 | 111 | Contributors SHOULD 112 | 113 | - update the CHANGELOG and README. 114 | - add tests for new functionality. 115 | 116 | If you submit a pull request that adheres to these rules, then it will almost 117 | certainly be merged immediately. However some things may require more 118 | consideration. If you add new dependencies, or significantly increase the API 119 | surface, then we need to decide if these changes are in line with the project's 120 | goals. In this case you can start by [writing a pitch](https://nextjournal.com/lambdaisland/pitch-template), 121 | and collecting feedback on it. 122 | 123 | `*` This goes for features too, a feature needs to solve a problem. State the problem it solves, then supply a minimal solution. 124 | 125 | `**` As long as this project has not seen a public release (i.e. is not on Clojars) 126 | we may still consider making breaking changes, if there is consensus that the 127 | changes are justified. 128 | 129 | 130 | 131 | ## License 132 | 133 | Copyright © 2021 Arne Brasseur and Contributors 134 | 135 | Licensed under the term of the Mozilla Public License 2.0, see LICENSE. 136 | 137 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {lambdaisland/open-source {:git/url "https://github.com/lambdaisland/open-source" 3 | :sha "7ce125cbd14888590742da7ab3b6be9bba46fc7a" 4 | #_#_:local/root "../open-source"}}} 5 | -------------------------------------------------------------------------------- /bin/proj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (ns proj 4 | (:require [lioss.main :as lioss])) 5 | 6 | (lioss/main 7 | {:license :mpl 8 | :inception-year 2021 9 | :description "Classpath utilities" 10 | :group-id "com.lambdaisland"}) 11 | 12 | ;; Local Variables: 13 | ;; mode:clojure 14 | ;; End: 15 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | 3 | :deps 4 | {rewrite-clj/rewrite-clj {:mvn/version "1.1.49"} 5 | org.clojure/tools.deps.alpha {:mvn/version "0.14.1222"} 6 | com.lambdaisland/shellutils {:mvn/version "0.3.20"} 7 | org.clojure/java.classpath {:mvn/version "1.0.0"} 8 | com.nextjournal/beholder {:mvn/version "1.0.2"} 9 | io.methvin/directory-watcher {:mvn/version "0.18.0"}}} 10 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.lambdaisland 5 | classpath 6 | 0.6.58 7 | classpath 8 | Classpath utilities 9 | https://github.com/lambdaisland/classpath 10 | 2021 11 | 12 | Lambda Island 13 | https://lambdaisland.com 14 | 15 | 16 | UTF-8 17 | 18 | 19 | 20 | MPL-2.0 21 | https://www.mozilla.org/media/MPL/2.0/index.txt 22 | 23 | 24 | 25 | https://github.com/lambdaisland/classpath 26 | scm:git:git://github.com/lambdaisland/classpath.git 27 | scm:git:ssh://git@github.com/lambdaisland/classpath.git 28 | cae7179067faeb0a97b661737034fe86c3b6bcd8 29 | 30 | 31 | 32 | rewrite-clj 33 | rewrite-clj 34 | 1.1.49 35 | 36 | 37 | org.clojure 38 | tools.deps.alpha 39 | 0.14.1222 40 | 41 | 42 | com.lambdaisland 43 | shellutils 44 | 0.3.20 45 | 46 | 47 | org.clojure 48 | java.classpath 49 | 1.0.0 50 | 51 | 52 | com.nextjournal 53 | beholder 54 | 1.0.2 55 | 56 | 57 | io.methvin 58 | directory-watcher 59 | 0.18.0 60 | 61 | 62 | 63 | src 64 | 65 | 66 | src 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-compiler-plugin 73 | 3.8.1 74 | 75 | 1.8 76 | 1.8 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-jar-plugin 82 | 3.2.0 83 | 84 | 85 | 86 | cae7179067faeb0a97b661737034fe86c3b6bcd8 87 | 88 | 89 | 90 | 91 | 92 | org.apache.maven.plugins 93 | maven-gpg-plugin 94 | 1.6 95 | 96 | 97 | sign-artifacts 98 | verify 99 | 100 | sign 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | clojars 110 | https://repo.clojars.org/ 111 | 112 | 113 | 114 | 115 | clojars 116 | Clojars repository 117 | https://clojars.org/repo 118 | 119 | 120 | -------------------------------------------------------------------------------- /repl_session/poke.clj: -------------------------------------------------------------------------------- 1 | (ns poke 2 | (:require [lambdaisland.classpath :as licp] 3 | [clojure.string :as str] 4 | [clojure.java.io :as io]) 5 | (:import clojure.lang.DynamicClassLoader)) 6 | 7 | (licp/classpath-chain) 8 | (licp/classloader-chain) 9 | 10 | (licp/update-classpath! {:extra {:paths ["repl_session/"]}}) 11 | 12 | (licp/debug! false) 13 | 14 | (licp/resources "poke.clj") 15 | 16 | (map (juxt licp/cl-id #(.getResource % "poke.clj")) 17 | (licp/classloader-chain)) 18 | 19 | (+ 1 1) 20 | 21 | (defn has-dcl? 22 | "Is this classloader or any of its ancestors a DynamicClassLoader?" 23 | ^DynamicClassLoader 24 | [^ClassLoader cl] 25 | (loop [loader cl] 26 | (when loader 27 | (if (instance? DynamicClassLoader loader) 28 | true 29 | (recur (.getParent loader)))))) 30 | 31 | (has-dcl? (licp/parent (licp/root-loader))) 32 | 33 | (keep (fn [{:local/keys [root]}] 34 | (when (and root (.exists (io/file root "shadow-cljs.edn"))) 35 | root)) 36 | (vals (:libs (licp/read-basis)))) 37 | 38 | (io/resource "public/index.html") 39 | -------------------------------------------------------------------------------- /src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdaisland/classpath/593d09e88af4a38ee328ba37b3e6fcf43cc96a49/src/.gitkeep -------------------------------------------------------------------------------- /src/lambdaisland/classpath.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.classpath 2 | (:require [rewrite-clj.zip :as z] 3 | [clojure.string :as str] 4 | [clojure.tools.deps.alpha :as deps] 5 | [clojure.tools.gitlibs :as gitlibs] 6 | [clojure.tools.gitlibs.impl :as gitlibs-impl] 7 | [clojure.java.classpath :as cp] 8 | [clojure.java.io :as io] 9 | [lambdaisland.shellutils :as shellutils]) 10 | (:import (java.util.jar JarFile JarEntry) 11 | (java.io File) 12 | (java.lang ClassLoader) 13 | (java.net URL URLClassLoader) 14 | (clojure.lang DynamicClassLoader))) 15 | 16 | #_(set! *warn-on-reflection* true) 17 | 18 | (defn read-basis 19 | "Read the basis (extended deps.edn) that Clojure started with, using the 20 | `clojure.basis` system property." 21 | [] 22 | (when-let [f (io/file (System/getProperty "clojure.basis"))] 23 | (if (and f (.exists f)) 24 | (deps/slurp-deps f) 25 | (throw (IllegalArgumentException. "No basis declared in clojure.basis system property"))))) 26 | 27 | (defn git-pull-lib* [loc lib] 28 | (let [coords (z/sexpr loc) 29 | git-url (:git/url coords) 30 | git-dir (gitlibs-impl/git-dir git-url) 31 | _ (when-not (.isDirectory (io/file git-dir)) 32 | (gitlibs-impl/git-clone-bare git-url git-dir)) 33 | _ (gitlibs-impl/git-fetch git-dir) 34 | sha (some #(and % (gitlibs-impl/git-rev-parse (str git-dir) %)) 35 | [(:git/branch coords) 36 | "main" 37 | "master"])] 38 | (z/assoc loc :git/sha sha))) 39 | 40 | (defn git-pull-lib 41 | "Update the :git/sha in deps.edn for a given library to the latest sha in a branch 42 | 43 | Uses `:git/branch` defaulting to `main`" 44 | ([lib] 45 | (git-pull-lib "deps.edn" lib)) 46 | ([deps-file lib] 47 | (let [loc (z/of-file deps-file)] 48 | (spit deps-file 49 | (z/root-string 50 | (as-> loc $ 51 | (if-let [loc (z/get (z/get $ :deps) lib)] 52 | (z/up (z/up (git-pull-lib* loc lib))) 53 | $) 54 | 55 | (if-let [aliases (some-> $ (z/get :aliases) z/sexpr)] 56 | (reduce (fn [loc alias] 57 | (reduce 58 | (fn [loc dep-type] 59 | (if-let [loc (some-> loc 60 | (z/get alias) 61 | (z/get dep-type) 62 | (z/get lib))] 63 | (z/up (z/up (z/up (git-pull-lib* loc lib)))) 64 | loc)) 65 | loc 66 | [:extra-deps :override-deps :default-deps])) 67 | $ 68 | (keys aliases)) 69 | $))))))) 70 | 71 | (defn classpath 72 | "clojure.java.classpath does not play well with the post-Java 9 application 73 | class loader, which is no longer a URLClassLoader, even though ostensibly it 74 | tries to cater for this, but in practice if any URLClassLoader or 75 | DynamicClassLoader higher in the chain contains a non-empty list of URLs, then 76 | this shadows the system classpath." 77 | [] 78 | (distinct (concat (cp/classpath) (cp/system-classpath)))) 79 | 80 | (defn context-classloader 81 | "Get the context classloader for the current thread" 82 | ^ClassLoader 83 | ([] 84 | (context-classloader (Thread/currentThread))) 85 | ([^Thread thread] 86 | (.getContextClassLoader thread))) 87 | 88 | (defn base-loader 89 | "Get the loader that Clojure uses internally to load files 90 | 91 | This is usually the current context classloader, but can also be 92 | clojure.lang.Compiler/LOADER." 93 | ^ClassLoader [] 94 | (clojure.lang.RT/baseLoader)) 95 | 96 | (defn cl-name 97 | "Get a classloaders's defined name" 98 | [^ClassLoader cl] 99 | (.getName cl)) 100 | 101 | (defn cl-id 102 | "return a symbol identifying the cl, mainly meant for concise printing" 103 | [^ClassLoader cl] 104 | (if cl 105 | (symbol 106 | (or (.getName cl) 107 | (str cl))) 108 | 'boot)) 109 | 110 | (defn dynamic-classloader? 111 | "Is the given classloader a [[clojure.lang.DynamicClassLoader]]" 112 | [cl] 113 | (instance? clojure.lang.DynamicClassLoader cl)) 114 | 115 | (defn priority-classloader? 116 | "Is the given classloader a [[priority-classloader]]" 117 | [cl] 118 | (when-let [name (and cl (cl-name cl))] 119 | (str/starts-with? name "lambdaisland/priority-classloader"))) 120 | 121 | (defn root-loader 122 | "Find the bottom-most DynamicClassLoader in the chain of parent classloaders" 123 | ^DynamicClassLoader 124 | ([] 125 | (root-loader (base-loader))) 126 | ([^ClassLoader cl] 127 | (when cl 128 | (loop [loader cl] 129 | (let [parent (.getParent loader)] 130 | (cond 131 | (or (dynamic-classloader? parent) 132 | (priority-classloader? parent)) 133 | (recur parent) 134 | 135 | (or (dynamic-classloader? loader) 136 | (priority-classloader? loader)) 137 | loader)))))) 138 | 139 | (defn app-loader 140 | "Get the application (aka system) classloader" 141 | ^ClassLoader [] 142 | (ClassLoader/getSystemClassLoader)) 143 | 144 | (defn platform-loader 145 | "Get the platform classloader" 146 | ^ClassLoader [] 147 | (ClassLoader/getPlatformClassLoader)) 148 | 149 | (defn compiler-loader 150 | "Get the clojure.lang.Compiler/LOADER, if set 151 | 152 | This is the loader Clojure uses to load code with, if the var is set. If not 153 | it falls back to the context classloader." 154 | [] 155 | @clojure.lang.Compiler/LOADER) 156 | 157 | (defn dynamic-classloader 158 | "Construct a new DynamicClassLoader" 159 | ^DynamicClassLoader 160 | [^ClassLoader parent] 161 | (clojure.lang.DynamicClassLoader. parent)) 162 | 163 | (defn parent 164 | "Get the parent classloader" 165 | ^ClassLoader [^ClassLoader cl] 166 | (.getParent cl)) 167 | 168 | (defn classpath-directories 169 | "Returns a sequence of File objects for the directories on classpath." 170 | [] 171 | (filter #(.isDirectory ^File %) (classpath))) 172 | 173 | (defn classpath-jarfiles 174 | "Returns a sequence of JarFile objects for the JAR files on classpath." 175 | [] 176 | (map #(JarFile. ^File %) (filter cp/jar-file? (classpath)))) 177 | 178 | (defn find-resources 179 | "Scan 'the classpath' for resources that match the given regex." 180 | [regex] 181 | ;; FIXME currently jar entries always come first in the result, this should be 182 | ;; in classpath order. 183 | (concat 184 | (sequence 185 | (comp 186 | (mapcat #(iterator-seq (.entries ^JarFile %))) 187 | (map #(.getName ^JarEntry %)) 188 | (filter #(re-find regex %))) 189 | (classpath-jarfiles)) 190 | 191 | (sequence 192 | (comp 193 | (mapcat file-seq) 194 | (map str) 195 | (filter #(re-find regex %))) 196 | (classpath-directories)))) 197 | 198 | (defn file->ns-name 199 | "Get the ns name for a given clj file name" 200 | [filename] 201 | (-> filename 202 | (str/replace #"\.clj$" "") 203 | (str/replace #"/" ".") 204 | (str/replace #"_" "-"))) 205 | 206 | (defn classloader-chain 207 | "Get the chain of parent classloaders, all the way to the system AppClassLoader 208 | and PlatformClassLoader." 209 | ([] 210 | (classloader-chain (base-loader))) 211 | ([cl] 212 | (take-while identity (iterate parent cl)))) 213 | 214 | (defn classpath-chain 215 | "Return a list of classloader names, and the URLs they have on their classpath 216 | 217 | Mainly meant for inspecting the current state of things." 218 | ([] 219 | (classpath-chain (context-classloader))) 220 | ([cl] 221 | (for [^ClassLoader cl (classloader-chain cl)] 222 | [(cl-id cl) 223 | (map str (cond 224 | (instance? URLClassLoader cl) 225 | (.getURLs ^URLClassLoader cl) 226 | (= "app" (.getName cl)) 227 | (cp/system-classpath)))]))) 228 | 229 | (defn resources 230 | "The plural of [[clojure.java.io/resource]], find all resources with the given 231 | name on the classpath. 232 | 233 | Useful for checking shadowing issues, in case a library ended up on the 234 | classpath multiple times." 235 | ([name] 236 | (resources (base-loader) name)) 237 | ([^ClassLoader cl name] 238 | (enumeration-seq (.getResources cl name)))) 239 | 240 | (defn priority-classloader 241 | "A modified URLClassloader 242 | 243 | It will give precedence to its own URLs over its parents, then to whatever 244 | resources its immediate parent returns, and only then passing the request up 245 | the chain, which will then proceed with the bottom most classloaders (Boot, 246 | then Platform, then App). 247 | 248 | We install this as the child of the bottom most 249 | clojure.lang.DynamicClassloader that we find. 250 | 251 | The logic here relies on the fact that DynamicClassLoader or its parent 252 | URLClassLoader do not implement the `getResource`/`getResources` methods, they 253 | rely on the parent implementation in ClassLoader, which gives precedence to 254 | ancestors, before proceeding to call `findResource`/`findResources`, which 255 | URLClassLoader/DynamicClassloader do implement. This classloader reverses that 256 | logic, so that the system classloader doesn't shadow our own classpath 257 | entries." 258 | [cl urls] 259 | (let [parent-loader (parent cl)] 260 | (proxy [URLClassLoader] [^String (str "lambdaisland/" 261 | (gensym "priority-classloader")) 262 | ^"[Ljava.net.URL;" (into-array URL urls) 263 | ^ClassLoader cl] 264 | (getResource [name] 265 | ;; `cl` is assumed to be the bottom-most DynamcClassLoader, which is 266 | ;; sitting directly above the application classloader 267 | ;; 268 | ;; - priority-classloader 269 | ;; - DynamicClassLoader 270 | ;; - app classloader 271 | ;; 272 | ;; The normal lookup order is bottom to top, we reverse that here by 273 | ;; first checking our own classpath, then the DCL, and only then handing 274 | ;; it to the app cl, which can further traverse down the chain. 275 | (or (.findResource this name) 276 | ;; reflection warning because findResource is protected, but we're a 277 | ;; subclass so it seems to be ok? 278 | (.findResource cl name) 279 | (.getResource parent-loader name))) 280 | (getResources [name] 281 | (java.util.Collections/enumeration 282 | (distinct 283 | (mapcat 284 | enumeration-seq 285 | ;; reflection warning because findResource is protected, but we're a 286 | ;; subclass so it seems to be ok? 287 | [(.findResources this name) 288 | (.findResources cl name) 289 | (.getResources parent-loader name)]))))))) 290 | 291 | (def fg-red "\033[0;31m") 292 | (def fg-green "\033[0;32m") 293 | (def fg-yellow "\033[0;33m") 294 | (def fg-blue "\033[0;34m") 295 | (def fg-reset "\033[0m") 296 | 297 | (defn debug-context-classloader* [ns meta-form ^Thread thread cl] 298 | (let [old-cl (context-classloader thread) 299 | chain (classloader-chain cl) 300 | old-chain (classloader-chain old-cl) 301 | merge-base (some (set old-chain) chain) 302 | short-id #(-> (cl-id %) 303 | (str/replace #".*@" "") 304 | (str/replace #".*/" "")) 305 | sym (symbol (str "cl-" (short-id cl)))] 306 | (intern 'user sym (constantly cl)) 307 | (println (str "[" (.getName thread) "]") 308 | (str fg-yellow ns ":" (:line meta-form) ":" (:column meta-form) 309 | fg-blue " (user/" sym ")" fg-reset)) 310 | (if (= cl old-cl) 311 | (do 312 | (println (str fg-yellow " No-op" fg-reset))) 313 | (do 314 | (run! 315 | #(println fg-red " - " (cl-id %) '-> (short-id (parent %)) fg-reset) 316 | (take-while #(not= merge-base %) old-chain)) 317 | (run! 318 | #(println fg-green " + " (cl-id %) '-> (short-id (parent %)) fg-reset) 319 | (take-while #(not= merge-base %) chain)))) 320 | (.setContextClassLoader thread cl))) 321 | 322 | (defmacro debug-context-classloader 323 | "Replace calls to `.setContextClassloader` with this to get insights into 324 | who/what/where/when/how is changing the classloader" 325 | [thread cl] 326 | `(debug-context-classloader* '~(ns-name *ns*) ~(meta &form) ~thread ~cl)) 327 | 328 | (defn debug? [] 329 | (= "true" (System/getProperty "lambdaisland.classpath.debug"))) 330 | 331 | (defn debug! 332 | ([] 333 | (debug! true)) 334 | ([enable?] 335 | (System/setProperty "lambdaisland.classpath.debug" (str enable?)))) 336 | 337 | (defn ensure-trailing-slash 338 | "URLClassPath looks for a trailing slash to determine whether something is a 339 | directory instead of a jar, so add trailing slashes to everything that doesn't 340 | look like a JAR." 341 | [^String path] 342 | (cond 343 | (or (.endsWith path ".jar") (.endsWith path "/")) 344 | path 345 | (.isDirectory (io/file path)) 346 | (str path "/") 347 | :else 348 | path)) 349 | 350 | (defn install-priority-loader! 351 | "Install the new priority loader as immediate parent of the bottom-most 352 | DynamicClassloader, discarding any further descendants. After this the chain is 353 | 354 | [priority-classloader 355 | DynamicClassLoader 356 | AppClassLoader 357 | PlatformClassLoader] 358 | 359 | Do this for every thread that has a DynamicClassLoader as the context 360 | classloader, or any of its parents. 361 | 362 | We need to do this from a separate thread, hence the `future` call, because 363 | nREPL's interruptible-eval resets the context-classloader at the end of the 364 | evaluation, so this needs to happen after that has happened. 365 | 366 | Start the JVM with `-Dlambdaisland.classpath.debug=true` to get debugging 367 | output. 368 | " 369 | ([] 370 | (install-priority-loader! [])) 371 | ([paths] 372 | (when (debug?) 373 | (println "Installing priority-classloader") 374 | (run! #(println "-" %) paths)) 375 | (let [urls (map #(URL. (str "file:" (ensure-trailing-slash %))) paths) 376 | current-thread (Thread/currentThread) 377 | dyn-cl (or (root-loader (context-classloader current-thread)) 378 | (clojure.lang.DynamicClassLoader. (app-loader))) 379 | new-loader (priority-classloader dyn-cl urls)] 380 | ;; Install a priority-classloader in every thread that currently has a 381 | ;; DynamicClassLoader 382 | (future 383 | (try 384 | (Thread/sleep 100) 385 | (doseq [^Thread thread (.keySet (Thread/getAllStackTraces)) 386 | ;; InnocuousThread#setContextClassLoader throws 387 | ;; SecurityException, so we skip those. We can't do an 388 | ;; `instance?` check because the module jdk.internal.misc is 389 | ;; private. 390 | :when (not (#{"class jdk.internal.misc.CarrierThread" 391 | "class jdk.internal.misc.InnocuousThread"} (str (class thread)))) 392 | ;; Install the new loader in every thread that has a Clojure 393 | ;; loader, and always in the thread this is invoked in, even if 394 | ;; for some reason it does not yet have a Clojure loader 395 | ;; 396 | ;; We also consider threads that currently have the application 397 | ;; loader set, this includes threads created by 398 | ;; futures/agents (clojure-agent-send-off-pool-*), before 399 | ;; `clojure.main/repl` installed its DynamicClassLoader 400 | :when (or (= thread current-thread) 401 | (root-loader (context-classloader thread)) 402 | (= (app-loader) (context-classloader thread)))] 403 | (if (debug?) 404 | (debug-context-classloader thread new-loader) 405 | (.setContextClassLoader thread new-loader))) 406 | (catch Exception e 407 | (println "Error in" `install-priority-loader! e) 408 | (.printStackTrace e)))) 409 | 410 | ;; Force orchard to use "our" classloader. This is a bit of nuclear option, 411 | ;; if we can clean up some of nREPLs classloader handling this should not 412 | ;; be necessary. 413 | #_(doseq [filename (find-resources #"orchard.*java/classpath.clj")] 414 | (try 415 | (alter-var-root 416 | (requiring-resolve (symbol (file->ns-name filename) "context-classloader")) 417 | (constantly (constantly new-loader))) 418 | (catch Exception e)))))) 419 | 420 | (defn update-classpath! 421 | "Use the given options to construct a basis (see [[deps/create-basis]]), then 422 | add any classpath-roots that aren't part of the system classpath yet to the 423 | classpath, by installing an extra classloader over Clojure's 424 | DynamicClassloader which takes precedence. 425 | 426 | This is the closest we can get to \"replacing\" the classpath. We can't remove 427 | any entries from the system classpath (the classpath the JVM booted with), but 428 | we can make sure any extra entries get precedence." 429 | [basis-opts] 430 | (install-priority-loader! 431 | (remove (set (map str (cp/system-classpath))) 432 | (:classpath-roots (deps/create-basis basis-opts))))) 433 | 434 | 435 | 436 | (comment 437 | (git-pull-lib 'com.lambdaisland/webstuff) 438 | 439 | (update-classpath! 440 | '{:aliases [:dev :test :licp] 441 | :extra {:deps {com.lambdaisland/webstuff {:local/root "/home/arne/github/lambdaisland/webstuff"}}}}) 442 | 443 | (classpath-chain) 444 | (resources "lambdaisland/webstuff/http.clj") 445 | (io/resource "lambdaisland/webstuff/http.clj") 446 | 447 | (classloader-chain) 448 | (classpath-chain) 449 | 450 | (io/resource "clojure/main.class") 451 | ;;=> #object[java.net.URL 0x3237dfe5 "jar:file:/home/arne/.m2/repository/org/clojure/clojure/1.10.3/clojure-1.10.3.jar!/clojure/main.class"] 452 | 453 | (.getResource ^ClassLoader loader "clojure/main.class") 454 | 455 | (defn xxx []) 456 | 457 | (.loadClass (clojure.lang.RT/baseLoader) "user$xxx") 458 | ;; user$xxx 459 | 460 | (.loadClass (ClassLoader/getPlatformClassLoader) "lambdaisland.classpath$xxx") 461 | ;; => java.lang.ClassNotFoundException 462 | 463 | 464 | (group-by second 465 | (map (juxt #(.getName %) #(some-> (.getClassLoader %) .getName)) 466 | (.modules (java.lang.ModuleLayer/boot)))) 467 | 468 | 469 | ) 470 | -------------------------------------------------------------------------------- /src/lambdaisland/classpath/watch_deps.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.classpath.watch-deps 2 | "Watch deps.edn for changes" 3 | (:require [clojure.java.classpath :as cp] 4 | [clojure.string :as str] 5 | [clojure.java.io :as io] 6 | [clojure.tools.deps.alpha :as deps] 7 | [lambdaisland.classpath :as licp] 8 | [nextjournal.beholder :as beholder]) 9 | (:import java.util.regex.Pattern 10 | java.nio.file.LinkOption 11 | java.nio.file.Paths 12 | java.nio.file.Path)) 13 | 14 | (def watcher (atom nil)) 15 | 16 | (defn path ^Path [root & args] 17 | (if (and (instance? Path root) (not (seq args))) 18 | root 19 | (Paths/get (str root) (into-array String args)))) 20 | 21 | (defn canonical-path [p] 22 | (.toRealPath (path p) (into-array LinkOption []))) 23 | 24 | (defn parent-path [p] 25 | (.getParent (path p))) 26 | 27 | (def process-root-path (canonical-path ".")) 28 | 29 | (defn basis 30 | "Default function for (re-)computing the tools.deps basis, which we then use to 31 | update the classpath. Delegates to [[deps/create-basis]], with one addition: 32 | if you include an `:extra` option which points at a file (string), then we 33 | also look in that file for a `:lambdaisland.classpath/aliases`, which are 34 | additional alias keys to load. This allows having a `deps.local.edn`, where 35 | you can change the aliases in use without restarting." 36 | [opts] 37 | (if-let [f (:basis-fn opts)] 38 | (f opts) 39 | (deps/create-basis 40 | (if (string? (:extra opts)) 41 | (update opts :aliases into (:lambdaisland.classpath/aliases 42 | (deps/slurp-deps (io/file (:extra opts))))) 43 | opts)))) 44 | 45 | (defn- on-event [deps-paths opts {:keys [type path]}] 46 | (locking watcher 47 | (when (and (= :modify type) 48 | ;; Before we used "." as the watch path, resulting in a 49 | ;; difference between mac, where the path would look like this 50 | ;; `/Users/x/project/./deps.edn`, vs Linux where the path would 51 | ;; look like this `./deps.edn`. 52 | ;; 53 | ;; We now turn `"."` into a canonical path before starting the 54 | ;; watcher, which means we get fully qualified filenames for both 55 | ;; in this equality check. 56 | (some #{path} deps-paths)) 57 | (try 58 | (println "[watch-deps] ✨ Reloading" 59 | (str (.relativize process-root-path path)) 60 | "✨") 61 | (let [added-paths (remove (set (map str (cp/system-classpath))) 62 | (:classpath-roots (basis opts)))] 63 | (when (not (seq added-paths)) 64 | (println "[watch-deps] No new libraries to add.")) 65 | (doseq [path added-paths] 66 | (println "[watch-deps] +" (str/replace path #"^.*/\.m2/repository/" ""))) 67 | (licp/install-priority-loader! added-paths)) 68 | (catch Exception e 69 | (println "[watch-deps] Error while reloading deps.edn") 70 | (println e)))))) 71 | 72 | (defn start! 73 | "Start a file system watcher to pick up changes in `deps.edn' 74 | 75 | Options are passed on to tools.deps when creating the basis, you probably want 76 | to at least put the `:aliases` you need in there. 77 | 78 | ``` 79 | (start! {:aliases [:dev :test]}) 80 | ``` 81 | " 82 | [opts] 83 | (swap! watcher 84 | (fn [w] 85 | (when w 86 | (println "Stopping existing `deps.edn' watchers") 87 | (run! beholder/stop w)) 88 | (let [basis (basis opts) 89 | deps-paths (cond-> [(path process-root-path "deps.edn")] 90 | (:include-local-roots? opts) 91 | (into (->> (vals (:libs basis)) 92 | (keep :local/root) 93 | (map canonical-path) 94 | (map #(path % "deps.edn")))) 95 | (string? (:extra opts)) 96 | (conj (canonical-path (:extra opts))) 97 | :always 98 | (concat (:watch-paths opts))) 99 | roots (group-by parent-path deps-paths)] 100 | (doall 101 | (for [[root deps-paths] roots] 102 | (beholder/watch 103 | (partial #'on-event deps-paths opts) 104 | (str root)))))))) 105 | 106 | (defn stop! 107 | "Stop a previously started watcher" 108 | [& _] 109 | (swap! watcher 110 | (fn [w] 111 | (run! beholder/stop w) 112 | nil))) 113 | 114 | (comment 115 | (start! {:aliases [:dev] 116 | :extra "deps.local.edn"}) 117 | 118 | (stop!) 119 | (deps/create-basis {:aliases [:backend] 120 | :extra '{cider/cider-nrepl #:mvn{:version "0.28.5"} 121 | refactor-nrepl/refactor-nrepl #:mvn{:version "3.5.2"}}}) 122 | (remove (set (map str (cp/system-classpath))) 123 | (:classpath-roots (deps/create-basis opts)))) 124 | --------------------------------------------------------------------------------