1 // This file defines some code for getting info about packages and installing them.
4 #include <openssl/evp.h>
10 #include "stdlib/optionals.h"
11 #include "stdlib/paths.h"
12 #include "stdlib/print.h"
13 #include "stdlib/simpleparse.h"
14 #include "stdlib/tables.h"
15 #include "stdlib/text.h"
22 #define xsystem(...) \
24 const char *cmd = String(__VA_ARGS__); \
25 int _status = system(cmd); \
26 if (!WIFEXITED(_status) || WEXITSTATUS(_status) != 0) { \
27 errx(1, "Failed to run command: %s", String(__VA_ARGS__)); \
31 #define xsystem_cleanup(tmpdir, ...) \
33 const char *cmd = String(__VA_ARGS__); \
34 int _status = system(cmd); \
35 if (!WIFEXITED(_status) || WEXITSTATUS(_status) != 0) { \
36 if (tmpdir) Path$remove(tmpdir, true); \
37 errx(1, "Failed to run command: %s", String(__VA_ARGS__)); \
41 static OptionalText_t file_digest(Path_t path) {
42 FILE *f = fopen(path, "rb");
43 if (!f) return NONE_TEXT;
45 EVP_MD_CTX *ctx = EVP_MD_CTX_new();
46 EVP_DigestInit_ex(ctx, EVP_sha256(), NULL);
48 unsigned char buf[65536];
50 while ((n = fread(buf, 1, sizeof(buf), f)) > 0)
51 EVP_DigestUpdate(ctx, buf, n);
54 unsigned char hash[EVP_MAX_MD_SIZE];
56 EVP_DigestFinal_ex(ctx, hash, &len);
59 const char *prefix = "sha256:";
60 char *ret = GC_MALLOC_ATOMIC(strlen(prefix) + 2 * len + 1);
62 p = stpcpy(p, prefix);
63 static const char hex[] = "0123456789abcdef";
64 for (size_t i = 0; i < len; i++) {
65 *p++ = hex[hash[i] >> 4];
66 *p++ = hex[hash[i] & 0xf];
69 return Text$from_str(ret);
72 Text_t get_package_name(Path_t lib_dir) {
73 Text_t name = Path$base_name(lib_dir);
74 name = Text$without_prefix(name, Text("tomo-"));
75 name = Text$without_suffix(name, Text("-tomo"));
79 static Text_t package_text(pkg_info_t pkg) {
80 Text_t text = Texts("[", pkg.name, "]\n");
81 for (int64_t i = 0; i < (int64_t)pkg.info.entries.length; i++) {
83 const char *key, *value;
84 } *entry = pkg.info.entries.data + i * pkg.info.entries.stride;
85 text = Texts(text, entry->key, "=", entry->value, "\n");
90 static OptionalPath_t try_install_package_from_file(pkg_info_t *pkg, const char *source, Path_t downloaded,
91 OptionalPath_t tmpdir);
93 static OptionalPath_t try_install_package_from_source(Path_t ini_file, pkg_info_t *pkg, const char *source,
94 bool ask_confirmation) {
95 if (source[0] == '.' || source[0] == '/' || source[0] == '~') {
96 Path_t source_path = Path$from_str(source);
97 if (!Path$exists(source_path)) {
98 print("No such file: ", source_path);
101 if (Path$is_directory(source_path, true)) {
102 if (Table$str_get(pkg->info, "digest") != NULL) {
103 // TODO: add support for automatically deleting digest with confirmation
104 fail("The package source for ", pkg->name, " is a directory, but the package has a digest.",
105 "\nSource directory packages cannot have a digest, so please delete the digest from this "
106 "package.\nSource: ",
109 xsystem("tomo -p ", source_path);
112 return try_install_package_from_file(pkg, source, Path$resolved(source, Path$parent(ini_file)), NULL);
116 if (ask_confirmation) {
117 OptionalText_t answer = ask(Texts("The package ", Text$quoted(Text$from_str(pkg->name), false, Text("\"")),
118 " is not installed.\nDo you want to install it from ",
119 Text$quoted(Text$from_str(source), false, Text("\"")), "? [Y/n] "),
121 if (!(answer.length == 0 || Text$equal_values(answer, Text("Y")) || Text$equal_values(answer, Text("y")))) {
122 print("Okay, not installing it!");
127 print("Installing ", Text$quoted(Text$from_str(pkg->name), false, Text("\"")), " from URL...");
129 Path_t tmpdir = Path$unique_directory(Path$from_text(Texts("/tmp/tomo-", pkg->name, "-XXXXXX")));
131 xsystem_cleanup(tmpdir, "curl --output-dir ", quoted(tmpdir), " -LJO ", quoted(source));
133 List_t children = Path$children(tmpdir, true);
134 if (children.length != 1) {
135 Path$remove(tmpdir, true);
136 print("Failed to download file ", pkg->name, " from: ", source);
140 Path_t downloaded = *(Path_t *)children.data;
141 return try_install_package_from_file(pkg, source, downloaded, tmpdir);
144 OptionalPath_t try_install_package_from_file(pkg_info_t *pkg, const char *source, Path_t downloaded,
145 OptionalPath_t tmpdir) {
147 OptionalText_t digest = file_digest(downloaded);
148 if (digest.tag == TEXT_NONE) {
149 if (tmpdir != NULL) Path$remove(tmpdir, true);
150 fail("Failed to compute digest for package ", pkg->name);
153 const char *required_digest = Table$str_get(pkg->info, "digest");
154 if (required_digest == NULL) {
155 Table$str_set(&pkg->info, "digest", Text$as_c_string(digest));
156 print("Added digest for ", pkg->name, ": ", digest);
158 if (!Text$equal_values(Text$from_str(required_digest), digest)) {
160 if (tmpdir != NULL) Path$remove(tmpdir, true);
161 fail("Mismatched digest sum for package ", pkg->name, "! Expected ", required_digest, " but got ", digest);
165 OptionalPath_t install_location =
166 Path$from_text(Texts(Text$from_str(TOMO_PATH), "/lib/tomo@", TOMO_VERSION, "/", digest));
168 Result_t result = Path$create_directory(install_location, 0755, true);
169 if (result.Failure.reason.tag != TEXT_NONE) {
170 if (tmpdir != NULL) Path$remove(tmpdir, true);
171 fail("Failed to make install directory: ", result.Failure.reason);
174 if (Path$has_extension(downloaded, Text(".tar.gz")) || Path$has_extension(downloaded, Text(".tgz"))
175 || Path$has_extension(downloaded, Text(".tar.xz")) || Path$has_extension(downloaded, Text(".txz"))
176 || Path$has_extension(downloaded, Text(".tar"))) {
177 xsystem_cleanup(tmpdir, "tar xf ", downloaded, " -C ", install_location);
178 } else if (Path$has_extension(downloaded, Text(".zip"))) {
179 xsystem_cleanup(tmpdir, "unzip ", downloaded, " -d ", install_location);
181 Path$remove(tmpdir, true);
182 fail("Unsupported package filetype: ", downloaded);
185 List_t installed_files = Path$children(install_location, true);
186 if (installed_files.length == 1
187 && Path$is_directory(*(Path_t *)installed_files.data, false)) { // Single top-level wrapper dir
188 Path_t top_level = *(Path_t *)installed_files.data;
189 List_t contents = Path$children(top_level, true);
190 for (int64_t i = 0; i < (int64_t)contents.length; i++) {
191 Path_t p = *(Path_t *)(contents.data + i * contents.stride);
192 result = Path$move(p, Path$child(install_location, Path$base_name(p)), false);
193 if (result.Failure.reason.tag != TEXT_NONE) {
194 if (tmpdir != NULL) Path$remove(tmpdir, true);
195 fail(result.Failure.reason);
198 result = Path$remove(top_level, true);
199 if (result.Failure.reason.tag != TEXT_NONE) {
200 if (tmpdir != NULL) Path$remove(tmpdir, true);
201 fail(result.Failure.reason);
205 xsystem_cleanup(tmpdir, "tomo -p ", install_location);
207 Path_t info = Path$child(install_location, Text("package_info.ini"));
208 Path$write(info, Texts("name=", pkg->name, "\nsource=", source, "\ndigest=", digest, "\n"), 0644);
210 // Always clean up tmpdir!
211 if (tmpdir != NULL) Path$remove(tmpdir, true);
213 // Add digest to the package.ini file if it wasn't already there
214 if (required_digest == NULL && digest.tag != TEXT_NONE) {
215 Table$str_set(&pkg->info, "digest", Text$as_c_string(digest));
216 print("Added digest for ", pkg->name, ": ", digest);
219 return install_location;
222 static OptionalPath_t try_install_package(Path_t ini_file, pkg_info_t *pkg, bool ask_confirmation) {
223 OptionalPath_t install_location = NULL;
224 const char *digest = Table$str_get(pkg->info, "digest");
226 install_location = Path$from_text(Texts(Text$from_str(TOMO_PATH), "/lib/tomo@", TOMO_VERSION, "/", digest));
227 if (Path$exists(install_location)) {
228 return install_location;
232 const char *source = Table$str_get(pkg->info, "source");
233 for (int i = 1; source; i++) {
234 install_location = try_install_package_from_source(ini_file, pkg, source, ask_confirmation);
235 if (install_location != NULL) return install_location;
236 const char *new_source_key = String("source-", i + 1);
237 source = Table$str_get(pkg->info, new_source_key);
239 fail("No source for package: ", pkg->name);
243 static OptionalPath_t get_package_install_location(Path_t ini_file, const char *name) {
244 OptionalClosure_t by_line = Path$by_line(ini_file);
245 if (by_line.fn == NULL) return NONE_PATH;
246 OptionalText_t (*next_line)(void *) = by_line.fn;
248 Text_t reformatted = EMPTY_TEXT;
249 for (OptionalText_t line; (line = next_line(by_line.userdata)).tag != TEXT_NONE;) {
250 if (Text$equal_values(line, Texts("[", name, "]"))) goto found_package;
251 reformatted = Texts(reformatted, line, "\n");
257 pkg_info_t pkg = {.name = name, .info = EMPTY_TABLE};
258 for (OptionalText_t line; (line = next_line(by_line.userdata)).tag != TEXT_NONE;) {
259 const char *line_str = Text$as_c_string(line);
260 const char *key = NULL, *value = NULL;
261 if (!strparse(line_str, &key, "=", &value)) {
262 Table$str_set(&pkg.info, key, value);
267 bool had_digest = Table$str_get(pkg.info, "digest") != NULL;
268 OptionalPath_t installed = try_install_package(ini_file, &pkg, true);
269 if (installed == NULL) return NULL;
271 if (!had_digest && Table$str_get(pkg.info, "digest") != NULL) {
272 reformatted = Texts(reformatted, package_text(pkg), "\n\n");
273 for (OptionalText_t line; (line = next_line(by_line.userdata)).tag != TEXT_NONE;) {
274 reformatted = Texts(reformatted, line, "\n");
276 reformatted = Texts(Text$trim(reformatted, Text(" \r\n\t"), true, true), "\n");
277 Result_t result = Path$write(ini_file, reformatted, 0644);
278 if (result.Failure.reason.tag != TEXT_NONE) {
279 fail(result.Failure.reason);
286 OptionalPath_t find_installed_package(ast_t *use) {
287 const char *name = Match(use, Use)->path;
290 Path_t file_package = Path$with_extension(Path$from_str(use->file->filename), Text(".packages.ini"), false);
291 OptionalPath_t installed = get_package_install_location(file_package, name);
292 if (installed != NULL) return installed;
296 Path_t local_package = Path$sibling(Path$from_str(use->file->filename), Text("packages.ini"));
297 OptionalPath_t installed = get_package_install_location(local_package, name);
298 if (installed != NULL) return installed;
302 Path_t tomo_default_packages =
303 Path$from_text(Texts(Text$from_str(TOMO_PATH), "/lib/tomo@", TOMO_VERSION, "/packages.ini"));
304 OptionalPath_t installed = get_package_install_location(tomo_default_packages, name);
305 if (installed != NULL) return installed;