123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 | // This file defines some code for getting info about packages and installing them.
#include <err.h>
#include <openssl/evp.h>
#include <stdlib.h>
#include <sys/wait.h>
#include "config.h"
#include "packages.h"
#include "stdlib/datatypes.h"
#include "stdlib/optionals.h"
#include "stdlib/paths.h"
#include "stdlib/print.h"
#include "stdlib/simpleparse.h"
#include "stdlib/tables.h"
#include "stdlib/text.h"
typedef struct {
const char *name;
Table_t info;
} pkg_info_t;
#define xsystem(...) \
({ \
const char *cmd = String(__VA_ARGS__); \
int _status = system(cmd); \
if (!WIFEXITED(_status) || WEXITSTATUS(_status) != 0) { \
errx(1, "Failed to run command: %s", String(__VA_ARGS__)); \
} \
})
#define xsystem_cleanup(tmpdir, ...) \
({ \
const char *cmd = String(__VA_ARGS__); \
int _status = system(cmd); \
if (!WIFEXITED(_status) || WEXITSTATUS(_status) != 0) { \
if (tmpdir) Path$remove(tmpdir, true); \
errx(1, "Failed to run command: %s", String(__VA_ARGS__)); \
} \
})
static OptionalText_t file_digest(Path_t path) {
FILE *f = fopen(path, "rb");
if (!f) return NONE_TEXT;
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL);
unsigned char buf[65536];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0)
EVP_DigestUpdate(ctx, buf, n);
fclose(f);
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int len;
EVP_DigestFinal_ex(ctx, hash, &len);
EVP_MD_CTX_free(ctx);
const char *prefix = "sha256:";
char *ret = GC_MALLOC_ATOMIC(strlen(prefix) + 2 * len + 1);
char *p = ret;
p = stpcpy(p, prefix);
static const char hex[] = "0123456789abcdef";
for (size_t i = 0; i < len; i++) {
*p++ = hex[hash[i] >> 4];
*p++ = hex[hash[i] & 0xf];
}
*p = '\0';
return Text$from_str(ret);
}
Text_t get_package_name(Path_t lib_dir) {
Text_t name = Path$base_name(lib_dir);
name = Text$without_prefix(name, Text("tomo-"));
name = Text$without_suffix(name, Text("-tomo"));
return name;
}
static Text_t package_text(pkg_info_t pkg) {
Text_t text = Texts("[", pkg.name, "]\n");
for (int64_t i = 0; i < (int64_t)pkg.info.entries.length; i++) {
struct {
const char *key, *value;
} *entry = pkg.info.entries.data + i * pkg.info.entries.stride;
text = Texts(text, entry->key, "=", entry->value, "\n");
}
return text;
}
static OptionalPath_t try_install_package_from_file(pkg_info_t *pkg, const char *source, Path_t downloaded,
OptionalPath_t tmpdir);
static OptionalPath_t try_install_package_from_source(Path_t ini_file, pkg_info_t *pkg, const char *source,
bool ask_confirmation) {
Table$str_set(&pkg->info, "source", source);
if (source[0] == '.' || source[0] == '/' || source[0] == '~') {
Path_t source_path = Path$from_str(source);
source_path = Path$resolved(source_path, Path$parent(ini_file));
if (!Path$exists(source_path)) {
print("No such file: ", source_path);
return NONE_PATH;
}
if (Path$is_directory(source_path, true)) {
if (Table$str_get(pkg->info, "digest") != NULL) {
// TODO: add support for automatically deleting digest with confirmation
fail("The package source for ", pkg->name, " is a directory, but the package has a digest.",
"\nSource directory packages cannot have a digest, so please delete the digest from this "
"package.\nSource: ",
source_path);
}
xsystem("tomo -p ", source_path);
return source_path;
} else {
return try_install_package_from_file(pkg, source, Path$resolved(source, Path$parent(ini_file)), NULL);
}
}
if (ask_confirmation) {
OptionalText_t answer = ask(Texts("The package ", Text$quoted(Text$from_str(pkg->name), false, Text("\"")),
" is not installed.\nDo you want to install it from ",
Text$quoted(Text$from_str(source), false, Text("\"")), "? [Y/n] "),
true, true);
if (!(answer.length == 0 || Text$equal_values(answer, Text("Y")) || Text$equal_values(answer, Text("y")))) {
print("Okay, not installing it!");
exit(1);
}
}
print("Installing ", Text$quoted(Text$from_str(pkg->name), false, Text("\"")), " from URL...");
Path_t tmpdir = Path$unique_directory(Path$from_text(Texts("/tmp/tomo-", pkg->name, "-XXXXXX")));
xsystem_cleanup(tmpdir, "curl --output-dir ", quoted(tmpdir), " -LJO ", quoted(source));
List_t children = Path$children(tmpdir, true);
if (children.length != 1) {
Path$remove(tmpdir, true);
print("Failed to download file ", pkg->name, " from: ", source);
return NONE_PATH;
}
Path_t downloaded = *(Path_t *)children.data;
return try_install_package_from_file(pkg, source, downloaded, tmpdir);
}
OptionalPath_t try_install_package_from_file(pkg_info_t *pkg, const char *source, Path_t downloaded,
OptionalPath_t tmpdir) {
OptionalText_t digest = file_digest(downloaded);
if (digest.tag == TEXT_NONE) {
if (tmpdir != NULL) Path$remove(tmpdir, true);
fail("Failed to compute digest for package ", pkg->name);
}
const char *required_digest = Table$str_get(pkg->info, "digest");
if (required_digest == NULL) {
Table$str_set(&pkg->info, "digest", Text$as_c_string(digest));
print("Added digest for ", pkg->name, ": ", digest);
} else {
if (!Text$equal_values(Text$from_str(required_digest), digest)) {
// Digest mismatch
if (tmpdir != NULL) Path$remove(tmpdir, true);
fail("Mismatched digest sum for package ", pkg->name, "! Expected ", required_digest, " but got ", digest);
}
}
OptionalPath_t install_location =
Path$from_text(Texts(Text$from_str(TOMO_PATH), "/lib/tomo@", TOMO_VERSION, "/", digest));
Result_t result = Path$create_directory(install_location, 0755, true);
if (result.Failure.reason.tag != TEXT_NONE) {
if (tmpdir != NULL) Path$remove(tmpdir, true);
fail("Failed to make install directory: ", result.Failure.reason);
}
if (Path$has_extension(downloaded, Text(".tar.gz")) || Path$has_extension(downloaded, Text(".tgz"))
|| Path$has_extension(downloaded, Text(".tar.xz")) || Path$has_extension(downloaded, Text(".txz"))
|| Path$has_extension(downloaded, Text(".tar"))) {
xsystem_cleanup(tmpdir, "tar xf ", downloaded, " -C ", install_location);
} else if (Path$has_extension(downloaded, Text(".zip"))) {
xsystem_cleanup(tmpdir, "unzip ", downloaded, " -d ", install_location);
} else {
Path$remove(tmpdir, true);
fail("Unsupported package filetype: ", downloaded);
}
List_t installed_files = Path$children(install_location, true);
if (installed_files.length == 1
&& Path$is_directory(*(Path_t *)installed_files.data, false)) { // Single top-level wrapper dir
Path_t top_level = *(Path_t *)installed_files.data;
List_t contents = Path$children(top_level, true);
for (int64_t i = 0; i < (int64_t)contents.length; i++) {
Path_t p = *(Path_t *)(contents.data + i * contents.stride);
result = Path$move(p, Path$child(install_location, Path$base_name(p)), false);
if (result.Failure.reason.tag != TEXT_NONE) {
if (tmpdir != NULL) Path$remove(tmpdir, true);
fail(result.Failure.reason);
}
}
result = Path$remove(top_level, true);
if (result.Failure.reason.tag != TEXT_NONE) {
if (tmpdir != NULL) Path$remove(tmpdir, true);
fail(result.Failure.reason);
}
}
xsystem_cleanup(tmpdir, "tomo -p ", install_location);
Path_t info = Path$child(install_location, Text("package_info.ini"));
Path$write(info, Texts("name=", pkg->name, "\nsource=", source, "\ndigest=", digest, "\n"), 0644);
// Always clean up tmpdir!
if (tmpdir != NULL) Path$remove(tmpdir, true);
// Add digest to the package.ini file if it wasn't already there
if (required_digest == NULL && digest.tag != TEXT_NONE) {
Table$str_set(&pkg->info, "digest", Text$as_c_string(digest));
print("Added digest for ", pkg->name, ": ", digest);
}
return install_location;
}
static OptionalPath_t try_install_package(Path_t ini_file, pkg_info_t *pkg, bool ask_confirmation) {
OptionalPath_t install_location = NULL;
const char *digest = Table$str_get(pkg->info, "digest");
if (digest) {
install_location = Path$from_text(Texts(Text$from_str(TOMO_PATH), "/lib/tomo@", TOMO_VERSION, "/", digest));
if (Path$exists(install_location)) {
return install_location;
}
}
const char *source = Table$str_get(pkg->info, "source");
for (int i = 1; source; i++) {
install_location = try_install_package_from_source(ini_file, pkg, source, ask_confirmation);
if (install_location != NULL) return install_location;
const char *new_source_key = String("source-", i + 1);
source = Table$str_get(pkg->info, new_source_key);
}
fail("No source for package: ", pkg->name);
return NULL;
}
static OptionalPath_t get_package_install_location(Table_t *build_info, Path_t ini_file, const char *name) {
OptionalClosure_t by_line = Path$by_line(ini_file);
if (by_line.fn == NULL) return NONE_PATH;
OptionalText_t (*next_line)(void *) = by_line.fn;
Text_t reformatted = EMPTY_TEXT;
for (OptionalText_t line; (line = next_line(by_line.userdata)).tag != TEXT_NONE;) {
if (Text$equal_values(line, Texts("[", name, "]"))) goto found_package;
reformatted = Texts(reformatted, line, "\n");
}
return NONE_PATH;
found_package:;
pkg_info_t pkg = {.name = name, .info = EMPTY_TABLE};
for (OptionalText_t line; (line = next_line(by_line.userdata)).tag != TEXT_NONE;) {
const char *line_str = Text$as_c_string(line);
const char *key = NULL, *value = NULL;
if (!strparse(line_str, &key, "=", &value)) {
Table$str_set(&pkg.info, key, value);
} else {
break;
}
}
bool had_digest = Table$str_get(pkg.info, "digest") != NULL;
OptionalPath_t installed = try_install_package(ini_file, &pkg, true);
if (installed == NULL) return NULL;
if (!had_digest && Table$str_get(pkg.info, "digest") != NULL) {
reformatted = Texts(reformatted, package_text(pkg), "\n");
for (OptionalText_t line; (line = next_line(by_line.userdata)).tag != TEXT_NONE;) {
reformatted = Texts(reformatted, line, "\n");
}
reformatted = Texts(Text$trim(reformatted, Text(" \r\n\t"), true, true), "\n");
Result_t result = Path$write(ini_file, reformatted, 0644);
if (result.Failure.reason.tag != TEXT_NONE) {
fail(result.Failure.reason);
}
}
const char *digest = Table$str_get(pkg.info, "digest");
Text_t digest_key = Texts("Package digest [", name, "]");
Table$str_set(build_info, Text$as_c_string(digest_key), digest);
Text_t source_key = Texts("Package source [", name, "]");
const char *source = Table$str_get(pkg.info, "source");
Table$str_set(build_info, Text$as_c_string(source_key), source);
return installed;
}
OptionalPath_t find_installed_package(Table_t *build_info, ast_t *use) {
const char *name = Match(use, Use)->path;
{
Path_t file_package = Path$with_extension(Path$from_str(use->file->filename), Text(".packages.ini"), false);
OptionalPath_t installed = get_package_install_location(build_info, file_package, name);
if (installed != NULL) return installed;
}
{
Path_t local_package = Path$sibling(Path$from_str(use->file->filename), Text("packages.ini"));
OptionalPath_t installed = get_package_install_location(build_info, local_package, name);
if (installed != NULL) return installed;
}
{
Path_t tomo_default_packages =
Path$from_text(Texts(Text$from_str(TOMO_PATH), "/lib/tomo@", TOMO_VERSION, "/packages.ini"));
OptionalPath_t installed = get_package_install_location(build_info, tomo_default_packages, name);
if (installed != NULL) return installed;
}
return NONE_PATH;
}
|