~ac/chnode

96174cf711bd1673f9f96db3b35d81f2e699c68e — Ant Cosentino 4 years ago 604b3b6
move everything from the entrypoint, make everything static where possible, use more structs and loops
1 files changed, 677 insertions(+), 34 deletions(-)

M chnode.h
M chnode.h => chnode.h +677 -34
@@ 1,39 1,110 @@

#ifdef __linux__
#define _GNU_SOURCE
#define UNAME "linux"
#elif defined __APPLE__
#define UNAME "darwin"
#elif defined __WIN64__
#include <windows.h>
#define mkdir(dir, mode) _mkdir(dir)
#define UNAME "win"
#else
#error "chnode does not support this operating system"
#endif

#ifndef PREFIX
#define PREFIX "/usr/local"
#warning "chnode is being installed with the default global prefix: /usr/local"
#endif

#define NODEJS_DIST_BASE_URI "https://nodejs.org/dist"

#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>

#include <curl/curl.h>

static const char* version[];
static struct chnode {
	bool is_error;
	bool is_recoverable;
	bool installing;
	bool restoring;

void on_exit() {
	const char* home = getenv("HOME");
	char* clean_cmd;
	struct args {
		bool help;
		bool current;
		bool use;
		bool version;
	} args;

	// TODO share the version here somehow
	bool format_error = asprintf(
		&clean_cmd,
		"rm -rf %s/.chnode/%s.%s.%s",
		home,
		version[0],
		version[1],
		version[2]
	) < 0;
	struct version {
		char* major;
		char* minor;
		char* patch;
	} version;

	int status = system(clean_cmd);
	if (status != 0) {
		printf(
			"Failed to clean up %s/.chnode/%s.%s.%s",
			home,
			version[0],
			version[1],
			version[2]
		);
	struct paths {
		char* node_src;
		char* npm_src;
		char* npx_src;

		char* node_dst;
		char* npm_dst;
		char* npx_dst;

		char* home;
		char* prefix;
		char* chnode;

		char* version;
		char* release;
		char* tarball;
		char* shasums;

		char* tarball_uri;
		char* shasums_uri;
	} paths;

} ctx;

static void on_exit() {
	#ifdef TRACE
	printf("TRACE: enter on_exit\n");
	#endif

	if (!ctx.is_error) return;
	if (!ctx.is_recoverable) {
		printf("WARNING: Detected an unrecoverable error\n");
		printf("WARNING: Consider nuking your chnode directory before retrying\n");
		return;
	}

	char* clean_cmd;
	bool format_error = asprintf(&clean_cmd, "rm -rf %s", ctx.paths.version) < 0;
	if (format_error) {
		printf("WARNING: Failed to clean up .chnode directory\n");
		return;
	}

	if (system(clean_cmd)) {
		printf("WARNING: Failed to clean up %s\n", ctx.paths.version);
		return;
	}
}

size_t curl_on_data(void* ptr, size_t bytes, size_t nmemb, void* stream) {
static size_t curl_on_data(void* ptr, size_t bytes, size_t nmemb, void* stream) {
	return fwrite(ptr, bytes, nmemb, (FILE*) stream);
}

int http_get_to_file(char* uri, FILE* f) {
static bool http_get_to_file(char* uri, FILE* f) {
	#ifdef TRACE
	printf("TRACE: enter http_get_to_file\n");
	#endif
	CURL* curl;
	curl_global_init(CURL_GLOBAL_ALL);
	curl = curl_easy_init();


@@ 51,16 122,588 @@ int http_get_to_file(char* uri, FILE* f) {
	);
}

bool parse_version(char* version, char* out[3]) {
	size_t version_ptr = 0;
static void show_intro(void) {
	#ifdef TRACE
	printf("TRACE: enter show_intro\n");
	#endif
	printf("chnode :: Install and use different versions of Node.js\n");
	printf("version 0.0.1\n");
	printf("Copyright (c) 2020 Ant Cosentino\n");
	printf("https://git.sr.ht/~ac/chnode\n");
}

static void show_usage(void) {
	#ifdef TRACE
	printf("TRACE: enter show_usage\n");
	#endif
	printf("Usage: chnode <version>\n");
}

static bool show_current() {
	#ifdef TRACE
	printf("TRACE: enter show_current\n");
	#endif
	bool format_error;
	int cmd_status;
	char* cmds[3];
	char* binaries[3] = {
		"bin/node",
		"bin/npm",
		"bin/npx"
	};

	for (size_t i = 0; i < 3; i += 1) {
		format_error = asprintf(
			&cmds[i],
			"ls -la %s/%s",
			PREFIX,
			binaries[i]
		) < 0;
		if (format_error) {
			perror("Failed to format command");
			ctx.is_error = true;
			return false;
		}

		cmd_status = system(cmds[i]);
		if (cmd_status > 0) {
			ctx.is_error = true;
			return false;
		}
	}

	return true;
}

static bool parse_version(char* version) {
	#ifdef TRACE
	printf("TRACE: enter parse_version\n");
	#endif
	size_t v_ptr = 0;
	char* versions[3];
	char* next_token = strtok(version, ".");
	if (!next_token) return true;
	out[version_ptr] = next_token;
	if (!next_token) return false;
	versions[v_ptr] = next_token;
	while ((next_token = strtok(NULL, "."))) {
		version_ptr += 1;
		if (version_ptr > 2) return true;
		out[version_ptr] = next_token;
		v_ptr += 1;
		if (v_ptr > 2) return false;
		versions[v_ptr] = next_token;
	}
	if (v_ptr != 2) return false;

	ctx.version.major = versions[0];
	ctx.version.minor = versions[1];
	ctx.version.patch = versions[2];

	// FIXME
	// the version can be a.b.c

	return true;
}

static bool chnode_dir() {
	#ifdef TRACE
	printf("TRACE: enter chnode_dir\n");
	#endif
	ctx.paths.home = getenv("HOME");
	bool format_error = asprintf(
		&ctx.paths.chnode,
		"%s/.chnode",
		ctx.paths.home
	) < 0;
	if (format_error) {
		perror("Failed to construct path to chnode directory");
		ctx.is_error = true;
		ctx.is_recoverable = false;
		return false;
	}

	bool mkdir_error = mkdir(ctx.paths.chnode, 0770) < 0;
	if (mkdir_error && errno != EEXIST) {
		perror("Failed to make chnode directory");
		ctx.is_error = true;
		ctx.is_recoverable = false;
		return false;
	}

	return true;
}

static bool version_dir() {
	#ifdef TRACE
	printf("TRACE: enter version_dir\n");
	#endif
	bool format_error = asprintf(
		&ctx.paths.version,
		"%s/%s.%s.%s",
		ctx.paths.chnode,
		ctx.version.major,
		ctx.version.minor,
		ctx.version.patch
	) < 0;

	if (format_error) {
		perror("Failed to construct path to directory for given version");
		ctx.is_error = true;
		ctx.is_recoverable = false;
		return false;
	}

	bool nodejs_path_error = access(ctx.paths.version, F_OK) < 0;
	bool not_exist = errno == ENOENT;

	if (not_exist) {
		bool mkdir_error = mkdir(ctx.paths.version, 0770) < 0;

		// in a race, this directory could already exist
		if (mkdir_error && errno != EEXIST) {
			perror("Failed to ensure version directory exists");
			ctx.is_error = true;
			ctx.is_recoverable = false;
			return false;
		}
		ctx.installing = true;
		return true;
	}

	if (nodejs_path_error) {
		perror("Failed to ensure version directory exists");
		ctx.is_error = true;
		ctx.is_recoverable = false;
		return false;
	}
	ctx.restoring = true;
	return true;
}

static bool release_dir() {
	#ifdef TRACE
	printf("TRACE: enter release_dir\n");
	#endif
	bool format_error = asprintf(
		&ctx.paths.release,
		"%s/release",
		ctx.paths.version
	) < 0;
	if (format_error) {
		perror("Failed to construct path to directory for given version");
		ctx.is_error = true;
		ctx.is_recoverable = false;
		return false;
	}

	bool nodejs_release_path_error = access(ctx.paths.release, F_OK) < 0;
	bool not_exist = errno == ENOENT;

	if (not_exist) {
		bool mkdir_error = mkdir(ctx.paths.release, 0770) < 0;
		if (mkdir_error && errno != EEXIST) {
			perror("Failed to ensure release directory exists");
			ctx.is_error = true;
			ctx.is_recoverable = false;
			return false;
		}
		return true;
	}

	if (nodejs_release_path_error) {
		perror("Failed to ensure release directory exists");
		ctx.is_error = true;
		ctx.is_recoverable = false;
		return false;
	}
	return true;
}

static bool download_and_verify() {
	#ifdef TRACE
	printf("TRACE: enter download_and_verify\n");
	#endif
	if (ctx.restoring) return true;

	bool format_error, downloaded;
	int cmd_status;
	FILE *nodejs_tarball, *nodejs_shasums;

	format_error = asprintf(
		&ctx.paths.tarball,
		"node-v%s.%s.%s-%s-x64.tar.gz",
		ctx.version.major,
		ctx.version.minor,
		ctx.version.patch,
		UNAME
	) < 0;

	if (format_error) {
		perror("Failed to construct tarball file path");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	format_error = asprintf(
		&ctx.paths.tarball_uri,
		"%s/v%s.%s.%s/%s",
		NODEJS_DIST_BASE_URI,
		ctx.version.major,
		ctx.version.minor,
		ctx.version.patch,
		ctx.paths.tarball
	) < 0;

	if (format_error) {
		perror("Failed to construct URI for given version");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	char* nodejs_download_path;
	format_error = asprintf(
		&nodejs_download_path,
		"%s/%s",
		ctx.paths.version,
		ctx.paths.tarball
	) < 0;

	if (format_error) {
		perror("Failed to construct path for file download");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	nodejs_tarball = fopen(nodejs_download_path, "wb");
	if (!nodejs_tarball) {
		perror("Failed to open file path for download");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	downloaded = http_get_to_file(ctx.paths.tarball_uri, nodejs_tarball);
	if (fclose(nodejs_tarball)) {
		perror("Failed to close reference to tarball");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}
	if (version_ptr != 2) return true;
	return false;
}
\ No newline at end of file
	if (!downloaded) {
		printf("Failed to download given version\n");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	format_error = asprintf(
		&ctx.paths.shasums_uri,
		"%s/v%s.%s.%s/SHASUMS256.txt",
		NODEJS_DIST_BASE_URI,
		ctx.version.major,
		ctx.version.minor,
		ctx.version.patch
	) < 0;
	if (format_error) {
		perror("Failed to construct URI to SHASUMS file");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	format_error = asprintf(
		&ctx.paths.shasums,
		"%s/SHASUMS256.txt",
		ctx.paths.version
	) < 0;
	if (format_error) {
		perror("Failed to construct path for file download");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	nodejs_shasums = fopen(ctx.paths.shasums, "wb");
	if (!nodejs_shasums) {
		perror("Failed to open file path for download");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	downloaded = http_get_to_file(ctx.paths.shasums_uri, nodejs_shasums);
	if (fclose(nodejs_shasums)) {
		perror("Failed to close reference to SHASUMS file");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}
	if (!downloaded) {
		printf("Failed to download given SHASUMS file\n");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	char* shasum_command;
	format_error = asprintf(
		&shasum_command,
		"cd %s && grep %s %s | sha256sum -c -",
		ctx.paths.version,
		ctx.paths.tarball,
		ctx.paths.shasums
	) < 0;

	cmd_status = system(shasum_command);
	if (cmd_status != 0) {
		printf("Failed to verify release signatures for given version. Exiting...\n");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	return true;
}

static bool extract_tarball() {
	#ifdef TRACE
	printf("TRACE: enter extract_tarball\n");
	#endif
	if (ctx.restoring) return true;

	char* tar_command;
	bool format_error;
	int cmd_status;

	format_error = asprintf(
		&tar_command,
		"tar -C %s/release --strip-components=1 -xf %s/%s",
		ctx.paths.version,
		ctx.paths.version,
		ctx.paths.tarball
	) < 0;
	if (format_error) {
		perror("Failed to construct command for extracting tarball");
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	cmd_status = system(tar_command);
	if (cmd_status == 127 || cmd_status == -1 || cmd_status != 0) {
		printf("Failed to extract tarball due to error %d. Exiting...\n", cmd_status);
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	printf("Tarball extracted to %s\n", ctx.paths.release);
	return true;
}

static bool unlink_symlinks() {
	#ifdef TRACE
	printf("TRACE: enter unlink_symlinks\n");
	#endif
	bool format_error;
	char* cmds[3];
	char* binaries[3] = {
		"node",
		"npm",
		"npx"
	};

	for (size_t i = 0; i < 3; i += 1) {
		format_error = asprintf(&cmds[i], "%s/bin/%s", PREFIX, binaries[i]) < 0;
		if (format_error) {
			ctx.is_error = true;
			ctx.is_recoverable = true;
			return false;
		}
		if (unlink(cmds[i]) == -1 && errno != ENOENT) {
			perror(cmds[i]);
			ctx.is_error = true;
			ctx.is_recoverable = true;
			return false;
		}
	}

	return true;
}

static bool mk_symlinks() {
	#ifdef TRACE
	printf("TRACE: enter mk_symlinks\n");
	#endif

	bool format_error, symlink_error;

	format_error = asprintf(&ctx.paths.node_src, "%s/bin/node", ctx.paths.release) < 0;
	if (format_error) {
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	format_error = asprintf(&ctx.paths.node_dst, "%s/bin/node", PREFIX) < 0;
	if (format_error) {
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	format_error = asprintf(&ctx.paths.npm_src, "%s/bin/npm", ctx.paths.release) < 0;
	if (format_error) {
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	format_error = asprintf(&ctx.paths.npm_dst, "%s/bin/npm", PREFIX) < 0;
	if (format_error) {
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	format_error = asprintf(&ctx.paths.npx_src, "%s/bin/npx", ctx.paths.release) < 0;
	if (format_error) {
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	format_error = asprintf(&ctx.paths.npx_dst, "%s/bin/npx", PREFIX) < 0;
	if (format_error) {
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	symlink_error = symlink(ctx.paths.node_src, ctx.paths.node_dst) < 0;
	if (symlink_error) {
		perror(ctx.paths.node_dst);
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	symlink_error = symlink(ctx.paths.npm_src, ctx.paths.npm_dst) < 0;
	if (symlink_error) {
		perror(ctx.paths.npm_dst);
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	symlink_error = symlink(ctx.paths.npx_src, ctx.paths.npx_dst) < 0;
	if (symlink_error) {
		perror(ctx.paths.npx_dst);
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	return true;
}

static bool test_binaries() {
	#ifdef TRACE
	printf("TRACE: enter test_binaries\n");
	#endif
	char* test_node;
	char* test_npm;
	bool format_error;

	format_error = asprintf(&test_node, "%s -v", ctx.paths.node_dst) < 0;
	if (format_error) {
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	format_error = asprintf(&test_npm, "%s -v", ctx.paths.npm_dst) < 0;
	if (format_error) {
		ctx.is_error = true;
		ctx.is_recoverable = true;
		return false;
	}

	int node_status = system(test_node);
	int npm_status = system(test_npm);

	return !(node_status && npm_status);
}

static bool parse_arguments(int argc, char** argv) {
	#ifdef TRACE
	printf("TRACE: enter parse_arguments\n");
	#endif
	// TODO
	// accept a command via stdin
	if (
		argc < 2 ||
		!strncmp(argv[1], "-h", 2) ||
		!strncmp(argv[1], "help", 4)
	) {
		ctx.args.help = true;
		return true;
	}

	if (atexit(on_exit)) {
		perror("Failed to register exit handler. Exiting...\n");
		return false;
	}

	if (!strncmp(argv[1], "use", 3)) {
		ctx.args.use = true;
		return true;
	}

	ctx.args.version = parse_version(argv[1]);
	return ctx.args.version;
}

int dispatch_command(int argc, char** argv) {
	#ifdef TRACE
	printf("TRACE: enter dispatch_command\n");
	#endif

	if (!parse_arguments(argc, argv)) return EXIT_FAILURE;
	if (!chnode_dir()) return EXIT_FAILURE;

	// TODO
	// dispatch with one variable and function pointers?
	if (ctx.args.help) {
		show_intro();
		show_usage();
		show_current();
		return EXIT_SUCCESS;
	}

	if (ctx.args.use) {
		// TODO
		// from stdin/argv
		return EXIT_SUCCESS;
	}

	if (ctx.args.version) {
		printf(
			"Using v%s.%s.%s...\n",
			ctx.version.major,
			ctx.version.minor,
			ctx.version.patch
		);

		if (
			version_dir() &&
			release_dir() &&
			download_and_verify() &&
			extract_tarball() &&
			unlink_symlinks() &&
			mk_symlinks() &&
			test_binaries()
		) return EXIT_SUCCESS;
	}

	return EXIT_FAILURE;
}