~ac/chnode

94b0166a366f19c490a779b8a53d226b0ad57d91 — Ant Cosentino 3 years ago d45294b
update readme, finish the main program flow, windows support WIP
4 files changed, 340 insertions(+), 88 deletions(-)

M .editorconfig
M Makefile
M README.md
M chnode.c
M .editorconfig => .editorconfig +4 -0
@@ 8,6 8,10 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
indent_style = space
indent_size = 2

[{*Makefile,*.mk}]
indent_style = tab
indent_size = 4

M Makefile => Makefile +4 -4
@@ 4,10 4,10 @@ chnode: *.c
		-fsanitize=undefined,nullability,integer \
		-Wall \
		-pedantic \
		-o $@ \
		$^ \
		$(shell curl-config --libs) \
		$^
		-o $@

clean:
	-rm -f *.o
	-rm -f chnode
	-rm -f *.o *.out chnode
	-rm -rf $$HOME/.chnode

M README.md => README.md +19 -5
@@ 1,9 1,23 @@

# chnode

A work in progress.
The, supposedly, best tools for this problem all seem to come with caveats.

* Depends on libcurl.
* Downloads the version of Node.js you specify.
* Symlinks it to somewhere on your `$PATH` (but hopefully not system-wide, if possible)
* Keeps all the downloads in your home directory (`~/.chnode/$version`)
There's `nvm`, but to use it you have to source it into your shell with something like:

```bash
  [ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh"
  [ -s "/usr/local/opt/nvm/etc/bash_completion" ] && . "/usr/local/opt/nvm/etc/bash_completion"
```

Having to manually (and even automatically) perform this step is disappointing. In no way whatsoever is this better than having a `$PATH`-searchable executable for doing the job. Some computers can't do this shell set-up step quickly, either, adding noticable delay for booting new shells. It even has the side-effect of polluting the shell with functions that are never used. Then there's the problem with how `nvm` updates itself. That is to say, it's outright broken in one particularly popular OS configuration. I do like how it can interpret release names like `lts/carbon`, though, and have it find the URL to the correct binary. That isn't an easy feature to provide. And how it uses rc files in projects, which is by far my favourite feature.

I tried to find other popular tools, and came across `nvs`, which is actually written in JavaScript. Really missing the point there, folks. Facepalm.

So, with all of that in mind, what I'm building here:

* Depends on libcurl
* Downloads the version of Node.js you specify (only with a version number, right now, unfortunately)
* Symlinks it to somewhere on your `$PATH`
* Keeps all the tarballs and binaries downloaded in your home directory (`~/.chnode/$version`); and
* Is just a single executable with no ahead-of-time set-up required, or complicated update flow (I'm looking at you, `nvm`)

M chnode.c => chnode.c +313 -79
@@ 4,28 4,46 @@
#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

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

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

#include <curl/curl.h>

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

// void http_get(char* uri, char* file_path) {

// }
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) {
	CURL* curl;
	curl_global_init(CURL_GLOBAL_ALL);
	curl = curl_easy_init();
	curl_easy_setopt(curl, CURLOPT_URL, uri);
	curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
	curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_on_data);
	curl_easy_setopt(curl, CURLOPT_WRITEDATA, f);
	CURLcode res = curl_easy_perform(curl);
	curl_easy_cleanup(curl);
	curl_global_cleanup();
	return (
		res != CURLE_HTTP_RETURNED_ERROR &&
		res == CURLE_OK
	);
}

// TODO
// * name this fn better


@@ 44,14 62,16 @@ size_t use(char* version, char* out[3]) {
	return 1;
}

// TODO
// delete directories created for failed downloads (ie. http 404)
int main(int argc, char** argv) {
	printf("chnode version 0.0.1\n");
	printf("Copyright (c) 2020 Ant Cosentino\n");
	printf("https://git.sr.ht/~ac/chnode\n\n");

	// was this invoked with no arguments or
	// one positional arg equivalent to -h?
	// was this invoked with no arguments or -h?
	if (argc < 2 || !strncmp(argv[1], "-h", 2)) {
		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");
		printf("Usage: chnode <version>\n");
		return EXIT_FAILURE;
	}


@@ 63,98 83,312 @@ int main(int argc, char** argv) {
		return EXIT_FAILURE;
	}

	printf("Using v%s.%s.%s\n", version[0], version[1], version[2]);
	printf("Using v%s.%s.%s...\n", version[0], version[1], version[2]);

	// the file path is likely yo be at most 22 characters long
	char nodejs_path[22];
	int path_format_result = snprintf(
		nodejs_path,
		sizeof nodejs_path,
		"~/.chnode/%s.%s.%s",
		version[0],
		version[1],
		version[2]
	);
	bool format_error, mkdir_error, symlink_error;

	if (path_format_result < 0) {
		printf(
			"Failed to construct path to directory for v%s.%s.%s. Exiting...\n",
			version[0],
			version[1],
			version[2]
		);
	const char* HOME = getenv("HOME");

	char* chnode_path;
	format_error = asprintf(&chnode_path, "%s/.chnode", HOME) < 0;

	if (format_error) {
		perror("Failed to construct path to chnode directory");
		free(chnode_path);
		return EXIT_FAILURE;
	}

	printf(
		"Checking for existing installation of v%s.%s.%s\n",
	mkdir_error = mkdir(chnode_path, 0770) < 0;
	if (mkdir_error && errno != EEXIST) {
		perror("Failed to make chnode directory");
		free(chnode_path);
		return EXIT_FAILURE;
	}

	char* nodejs_path;
	format_error = asprintf(
		&nodejs_path,
		"%s/%s.%s.%s",
		chnode_path,
		version[0],
		version[1],
		version[2]
	);
	) < 0;

	if (format_error) {
		perror("Failed to construct path to directory for given version");
		free(chnode_path);
		return EXIT_FAILURE;
	}

	if (access(nodejs_path, F_OK) == -1) {
		printf(
			"v%s.%s.%s not found. Downloading...\n",
	char* nodejs_release_path;
	format_error = asprintf(&nodejs_release_path, "%s/release", nodejs_path) < 0;
	if (format_error) {
		perror("Failed to construct path to directory for given version");
		free(chnode_path);
		free(nodejs_path);
		return EXIT_FAILURE;
	}

	bool nodejs_path_exists = !access(nodejs_path, F_OK);
	if (!nodejs_path_exists) {

		mkdir_error = mkdir(nodejs_path, 0770) < 0;
		if (mkdir_error) {
			perror("Failed to make release parent directory");
			free(chnode_path);
			return EXIT_FAILURE;
		}

		mkdir_error = mkdir(nodejs_release_path, 0770) < 0;
		if (mkdir_error) {
			perror("Failed to make release directory");
			free(chnode_path);
			free(nodejs_path);
			free(nodejs_release_path);
			return EXIT_FAILURE;
		}

		char* nodejs_tarball_path;
		format_error = asprintf(
			&nodejs_tarball_path,
			"node-v%s.%s.%s-%s-x64.tar.gz",
			version[0],
			version[1],
			version[2]
		);
			version[2],
			UNAME
		) < 0;

		// TODO
		// download this release to ~/.chnode
		if (format_error) {
			perror("Failed to construct tarball file path");
			free(chnode_path);
			free(nodejs_path);
			free(nodejs_tarball_path);
			return EXIT_FAILURE;
		}

		// the uri is likely to be at most 75 characters long
		char uri[75];
		int uri_format_result = snprintf(
			uri,
			sizeof uri,
		char* nodejs_uri;
		format_error = asprintf(
			&nodejs_uri,
			// https://nodejs.org/dist/latest-v12.x/node-v12.14.1-linux-arm64.tar.gz
			"%slatest-v%s.x/node-v%s.%s.%s-%s-x64.tar.gz",
			"%s/latest-v%s.x/%s",
			NODEJS_DIST_BASE_URI,
			version[0],
			version[0],
			version[1],
			version[2],
			UNAME
		);

		if (uri_format_result < 0) {
			printf(
				"Failed to construct URI for version %s.%s.%s and uname %s. Exiting...\n",
				version[0],
				version[1],
				version[2],
				UNAME
			);
			nodejs_tarball_path
		) < 0;

		if (format_error) {
			perror("Failed to construct URI for given version");
			free(chnode_path);
			free(nodejs_path);
			free(nodejs_tarball_path);
			free(nodejs_uri);
			return EXIT_FAILURE;
		}

		char* nodejs_download_path;
		format_error = asprintf(
			&nodejs_download_path,
			"%s/%s",
			nodejs_path,
			nodejs_tarball_path
		) < 0;

		if (format_error) {
			perror("Failed to construct path for file download");
			free(chnode_path);
			free(nodejs_path);
			free(nodejs_tarball_path);
			free(nodejs_uri);
			free(nodejs_download_path);
			return EXIT_FAILURE;
		}

		FILE* nodejs_tarball;
		nodejs_tarball = fopen(nodejs_download_path, "wb");
		if (!nodejs_tarball) {
			perror("Failed to open file path for download");
			free(chnode_path);
			free(nodejs_path);
			free(nodejs_tarball_path);
			free(nodejs_uri);
			free(nodejs_download_path);
			return EXIT_FAILURE;
		}

		bool downloaded = http_get_to_file(nodejs_uri, nodejs_tarball);
		if (!downloaded) {
			free(chnode_path);
			free(nodejs_path);
			free(nodejs_tarball_path);
			free(nodejs_uri);
			free(nodejs_download_path);
			return EXIT_FAILURE;
		}

		printf("OK\n");
		// TODO
		// check the return code
		fclose(nodejs_tarball);

		char* tar_command;
		format_error = asprintf(
			&tar_command,
			"tar -C %s/release --strip-components=1 -xf %s/%s",
			nodejs_path,
			nodejs_path,
			nodejs_tarball_path
		) < 0;

		if (format_error) {
			perror("Failed to construct command for extracting tarball");
			free(chnode_path);
			free(nodejs_path);
			free(nodejs_tarball_path);
			free(nodejs_uri);
			free(nodejs_download_path);
			return EXIT_FAILURE;
		}

		int code = system(tar_command);
		if (code == 127 || code == -1 || code != 0) {
			printf("Failed to extract tarball due to error %d. Exiting...\n", code);
			free(chnode_path);
			free(nodejs_path);
			free(nodejs_tarball_path);
			free(nodejs_uri);
			free(nodejs_download_path);
			free(tar_command);
			return EXIT_FAILURE;
		} else {
			printf("Tarball extracted to %s\n", nodejs_release_path);
		}

		free(nodejs_tarball_path);
		free(nodejs_uri);
		free(nodejs_download_path);
		free(tar_command);

		// TODO
		// verify the release signatures
	}

	// TODO
	// set the symlinks up to point to the release
	bool unlink_node_error = (
		unlink("/usr/local/bin/node") == -1 &&
		errno != ENOENT
	);
	if (unlink_node_error) {
		perror("Failed to unlink node symlink");
		free(chnode_path);
		free(nodejs_path);
		free(nodejs_release_path);
		return EXIT_FAILURE;
	}

	return EXIT_SUCCESS;
	bool unlink_npm_error = (
		unlink("/usr/local/bin/npm") == -1 &&
		errno != ENOENT
	);
	if (unlink_npm_error) {
		perror("Failed to unlink npm symlink");
		free(chnode_path);
		free(nodejs_path);
		free(nodejs_release_path);
		return EXIT_FAILURE;
	}

	bool unlink_npx_error = (
		unlink("/usr/local/bin/npx") == -1 &&
		errno != ENOENT
	);
	if (unlink_npx_error) {
		perror("Failed to unlink npm symlink");
		free(chnode_path);
		free(nodejs_path);
		free(nodejs_release_path);
		return EXIT_FAILURE;
	}

	printf("Creating symbolic links...\n");

	char* node;
	format_error = asprintf(&node, "%s/bin/node", nodejs_release_path) < 0;
	if (format_error) {
		perror("Failed to construct path to binary");
		free(chnode_path);
		free(nodejs_path);
		free(nodejs_release_path);
		return EXIT_FAILURE;
	}

	symlink_error = symlink(node, "/usr/local/bin/node") < 0;
	if (symlink_error) {
		perror("Failed to symlink node");
		free(chnode_path);
		free(nodejs_path);
		free(nodejs_release_path);
		free(node);
		return EXIT_FAILURE;
	}

	// // curl_global_init(CURL_GLOBAL_DEFAULT);
	// // CURL* curl = curl_easy_init();
	char* npm;
	format_error = asprintf(&npm, "%s/bin/npm", nodejs_release_path) < 0;
	if (format_error) {
		perror("Failed to construct path to binary");
		free(chnode_path);
		free(nodejs_path);
		free(nodejs_release_path);
		free(node);
		return EXIT_FAILURE;
	}

	// // if (!curl) {
	// // 	curl_global_cleanup();
	// // 	printf("Failed to initialise libcurl. Exiting...\n");
	// // 	return EXIT_FAILURE;
	// // }
	symlink_error = symlink(npm, "/usr/local/bin/npm") < 0;
	if (symlink_error) {
		perror("Failed to symlink npm");
		free(chnode_path);
		free(nodejs_path);
		free(nodejs_release_path);
		free(node);
		free(npm);
		return EXIT_FAILURE;
	}

	// // curl_easy_setopt(curl, CURLOPT_URL, uri);
	char* npx;
	format_error = asprintf(&npx, "%s/bin/npx", nodejs_release_path) < 0;
	if (format_error) {
		perror("Failed to construct path to binary");
		free(chnode_path);
		free(nodejs_path);
		free(nodejs_release_path);
		free(node);
		free(npm);
		return EXIT_FAILURE;
	}
	symlink_error = symlink(npx, "/usr/local/bin/npx") < 0;
	if (symlink_error) {
		free(chnode_path);
		free(nodejs_path);
		free(nodejs_release_path);
		free(node);
		free(npm);
		free(npx);
		return EXIT_FAILURE;
	}

	// // CURLcode res = curl_easy_perform(curl);
	printf("OK\n");

	// // // TODO
	// // // store the tarball in a directory managed by chnode (maybe it should
	// // // be a dot-dir owned by the current user?)
	free(chnode_path);
	free(nodejs_path);
	free(nodejs_release_path);
	free(node);
	free(npm);
	free(npx);

	int node_version = system("/usr/local/bin/node -v");
	int npm_version = system("/usr/local/bin/npm -v");

	// return EXIT_SUCCESS;
	return (
		node_version || npm_version ?
		EXIT_FAILURE :
		EXIT_SUCCESS
	);
}