211 lines
8.8 KiB
Clojure
211 lines
8.8 KiB
Clojure
(ns babashka.pods.impl.resolver
|
|
{:no-doc true}
|
|
(:refer-clojure :exclude [resolve])
|
|
(:require [clojure.edn :as edn]
|
|
[clojure.java.io :as io]
|
|
[clojure.java.shell :refer [sh]]
|
|
[clojure.string :as str])
|
|
(:import [java.net URL HttpURLConnection]
|
|
[java.nio.file Files]))
|
|
|
|
(set! *warn-on-reflection* true)
|
|
|
|
(defn normalize-arch [arch]
|
|
(if (= "amd64" arch)
|
|
"x86_64"
|
|
arch))
|
|
|
|
(def os {:os/name (System/getProperty "os.name")
|
|
:os/arch (let [arch (System/getProperty "os.arch")]
|
|
(normalize-arch arch))})
|
|
|
|
(defn warn [& strs]
|
|
(binding [*out* *err*]
|
|
(apply println strs)))
|
|
|
|
(defn match-artifacts
|
|
([package] (match-artifacts package (:os/arch os)))
|
|
([package arch]
|
|
(let [artifacts (:pod/artifacts package)
|
|
res (filter (fn [{os-name :os/name
|
|
os-arch :os/arch}]
|
|
(let [os-arch (normalize-arch os-arch)]
|
|
(and (re-matches (re-pattern os-name) (:os/name os))
|
|
(re-matches (re-pattern os-arch)
|
|
arch))))
|
|
artifacts)]
|
|
(if (empty? res)
|
|
(if (and (= "Mac OS X" (:os/name os))
|
|
(= "aarch64" (:os/arch os)))
|
|
;; Rosetta2 fallback on Apple M1 machines
|
|
(match-artifacts package "x86_64")
|
|
(throw (IllegalArgumentException. (format "No executable found for pod %s (%s) and OS %s/%s"
|
|
(:pod/name package)
|
|
(:pod/version package)
|
|
(:os/name os)
|
|
(:os/arch os)))))
|
|
res))))
|
|
|
|
(defn unzip [{:keys [^java.io.File zip-file
|
|
^java.io.File destination-dir
|
|
verbose]}]
|
|
(when verbose (warn "Unzipping" (.getPath zip-file) "to" (.getPath destination-dir)))
|
|
(let [output-path (.toPath destination-dir)
|
|
zip-file (io/file zip-file)
|
|
_ (.mkdirs (io/file destination-dir))]
|
|
(with-open
|
|
[fis (Files/newInputStream (.toPath zip-file) (into-array java.nio.file.OpenOption []))
|
|
zis (java.util.zip.ZipInputStream. fis)]
|
|
(loop []
|
|
(let [entry (.getNextEntry zis)]
|
|
(when entry
|
|
(let [entry-name (.getName entry)
|
|
new-path (.resolve output-path entry-name)]
|
|
(if (.isDirectory entry)
|
|
(Files/createDirectories new-path ^"[Ljava.nio.file.attribute.FileAttribute;"
|
|
(into-array java.nio.file.attribute.FileAttribute []))
|
|
(Files/copy ^java.io.InputStream zis
|
|
new-path
|
|
^"[Ljava.nio.file.CopyOption;"
|
|
(into-array
|
|
java.nio.file.CopyOption
|
|
[java.nio.file.StandardCopyOption/REPLACE_EXISTING]))))
|
|
(recur)))))))
|
|
|
|
(defn un-tgz [^java.io.File zip-file ^java.io.File destination-dir verbose?]
|
|
(when verbose? (warn "Unzipping" (.getPath zip-file) "to" (.getPath destination-dir)))
|
|
(let [tmp-file (java.io.File/createTempFile "glam" ".tar")
|
|
output-path (.toPath tmp-file)]
|
|
(with-open
|
|
[fis (Files/newInputStream (.toPath zip-file) (into-array java.nio.file.OpenOption []))
|
|
zis (java.util.zip.GZIPInputStream. fis)]
|
|
(Files/copy ^java.io.InputStream zis
|
|
output-path
|
|
^"[Ljava.nio.file.CopyOption;"
|
|
(into-array
|
|
[java.nio.file.StandardCopyOption/REPLACE_EXISTING])))
|
|
(sh "tar" "xf" (.getPath tmp-file) "--directory" (.getPath destination-dir))
|
|
(.delete tmp-file)))
|
|
|
|
(defn make-executable [dest-dir executables verbose?]
|
|
(doseq [e executables]
|
|
(let [f (io/file dest-dir e)]
|
|
(when verbose? (warn "Making" (.getPath f) "executable."))
|
|
(.setExecutable f true))))
|
|
|
|
(defn download [source ^java.io.File dest verbose?]
|
|
(when verbose? (warn "Downloading" source "to" (.getPath dest)))
|
|
(let [source (URL. source)
|
|
dest (io/file dest)
|
|
conn ^HttpURLConnection (.openConnection ^URL source)]
|
|
(.setInstanceFollowRedirects conn true)
|
|
(.connect conn)
|
|
(io/make-parents dest)
|
|
(with-open [is (.getInputStream conn)]
|
|
(io/copy is dest))))
|
|
|
|
(def pod-manifests-dir
|
|
;; wrapped in delay for GraalVM native-image
|
|
(delay (io/file (or (System/getenv "XDG_DATA_HOME")
|
|
(System/getProperty "user.home"))
|
|
".babashka" "pods" "repository")))
|
|
|
|
(defn github-url [qsym version]
|
|
(format
|
|
"https://raw.githubusercontent.com/babashka/pod-registry/master/manifests/%s/%s/manifest.edn"
|
|
qsym version))
|
|
|
|
(defn pod-manifest
|
|
[qsym version force?]
|
|
(let [f (io/file @pod-manifests-dir (str qsym) (str version) "manifest.edn")]
|
|
(if (and (not force?)
|
|
(.exists f))
|
|
(edn/read-string (slurp f))
|
|
(do (download (github-url qsym version) f false)
|
|
(edn/read-string (slurp f))))))
|
|
|
|
(defn cache-dir
|
|
^java.io.File
|
|
[{pod-name :pod/name
|
|
pod-version :pod/version}]
|
|
(let [base-file
|
|
(if-let [pods-dir (System/getenv "BABASHKA_PODS_DIR")]
|
|
(io/file pods-dir)
|
|
(io/file (or
|
|
(System/getenv "XDG_CACHE_HOME")
|
|
(System/getProperty "user.home"))
|
|
".babashka"
|
|
"pods"))]
|
|
(io/file base-file
|
|
"repository"
|
|
(str pod-name)
|
|
pod-version)))
|
|
|
|
(defn data-dir
|
|
^java.io.File
|
|
[{pod-name :pod/name
|
|
pod-version :pod/version}]
|
|
(let [base-file
|
|
(if-let [pods-dir (System/getenv "BABASHKA_PODS_DIR")]
|
|
(io/file pods-dir)
|
|
(io/file (or
|
|
(System/getenv "XDG_DATA_HOME")
|
|
(System/getProperty "user.home"))
|
|
".babashka"
|
|
"pods"))]
|
|
(io/file base-file
|
|
"repository"
|
|
(str pod-name)
|
|
pod-version)))
|
|
|
|
(defn sha256 [file]
|
|
(let [buf (byte-array 8192)
|
|
digest (java.security.MessageDigest/getInstance "SHA-256")]
|
|
(with-open [bis (io/input-stream (io/file file))]
|
|
(loop []
|
|
(let [count (.read bis buf)]
|
|
(when (pos? count)
|
|
(.update digest buf 0 count)
|
|
(recur)))))
|
|
(-> (.encode (java.util.Base64/getEncoder)
|
|
(.digest digest))
|
|
(String. "UTF-8"))))
|
|
|
|
(defn resolve [qsym version force?]
|
|
(when-not (string? version)
|
|
(throw (IllegalArgumentException. "Version must be provided for resolving from pod registry!")))
|
|
(when-let [manifest (pod-manifest qsym version force?)]
|
|
(let [artifacts (match-artifacts manifest)
|
|
cdir (cache-dir manifest)
|
|
ddir (data-dir manifest)
|
|
execs (mapv (fn [artifact]
|
|
(let [url (:artifact/url artifact)
|
|
file-name (last (str/split url #"/"))
|
|
cache-file (io/file cdir file-name)
|
|
executable (io/file ddir (:artifact/executable artifact))]
|
|
(when (or force? (not (.exists executable)))
|
|
(warn (format "Downloading pod %s (%s)" qsym version))
|
|
(download url cache-file false)
|
|
(when-let [expected-sha (:artifact/hash artifact)]
|
|
(let [sha (sha256 cache-file)]
|
|
(when-not (= (str/replace expected-sha #"^sha256:" "")
|
|
sha)
|
|
(throw (ex-info (str "Wrong SHA-256 for file" (str cache-file))
|
|
{:sha sha
|
|
:expected-sha expected-sha})))))
|
|
(let [filename (.getName cache-file)]
|
|
(cond (str/ends-with? filename ".zip")
|
|
(unzip {:zip-file cache-file
|
|
:destination-dir ddir
|
|
:verbose false})
|
|
(or (str/ends-with? filename ".tgz")
|
|
(str/ends-with? filename ".tar.gz"))
|
|
(un-tgz cache-file ddir
|
|
false))
|
|
(.delete cache-file))
|
|
(make-executable ddir [(:artifact/executable artifact)] false)
|
|
(warn (format "Successfully installed pod %s (%s)" qsym version))
|
|
(io/file ddir (:artifact/executable artifact)))
|
|
(io/file ddir (:artifact/executable artifact)))) artifacts)]
|
|
{:executable (.getAbsolutePath ^java.io.File (first execs))
|
|
:options (:pod/options manifest)})))
|