pig.py/bootstrap.c

384 lines
11 KiB
C

/*
* bootstrap.c - Self-extracting archive bootstrap
*
* A small C program that boots neopig from an embedded tar.gz archive.
* When compiled and concatenated with a tar.gz archive, it:
* 1. Extracts neopig/*.py from the embedded tarball to /tmp
* 2. Runs: python3 /tmp/neopig/serp.py <self>
* 3. The Python script serves directly from the tarball portion
*
* Build:
* gcc -O2 -o bootstrap bootstrap.c -lz
*
* Create self-extracting archive:
* cat bootstrap archive.tar.gz > archive.run
* chmod +x archive.run
* echo -n "NEOPIG" >> archive.run # Magic marker
* printf '%016x' $(stat -c%s bootstrap) >> archive.run # Offset (hex)
*
* Or use: make run TARBALL=archive.tar.gz
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <errno.h>
#include <zlib.h>
#define MAGIC "NEOPIG"
#define MAGIC_LEN 6
#define OFFSET_LEN 16
#define TRAILER_LEN (MAGIC_LEN + OFFSET_LEN)
#define CHUNK_SIZE 16384
/* Find python3 interpreter */
static const char *find_python(void) {
static const char *pythons[] = {
"/usr/bin/python3",
"/usr/local/bin/python3",
"/opt/homebrew/bin/python3",
"python3",
NULL
};
for (int i = 0; pythons[i]; i++) {
if (access(pythons[i], X_OK) == 0) {
return pythons[i];
}
}
/* Try PATH lookup */
return "python3";
}
/* Read trailer to get tarball offset */
static long read_trailer(const char *self_path) {
FILE *f = fopen(self_path, "rb");
if (!f) {
perror("fopen self");
return -1;
}
/* Seek to trailer */
if (fseek(f, -TRAILER_LEN, SEEK_END) != 0) {
perror("fseek trailer");
fclose(f);
return -1;
}
char trailer[TRAILER_LEN + 1];
if (fread(trailer, 1, TRAILER_LEN, f) != TRAILER_LEN) {
perror("fread trailer");
fclose(f);
return -1;
}
trailer[TRAILER_LEN] = '\0';
fclose(f);
/* Check magic */
if (memcmp(trailer, MAGIC, MAGIC_LEN) != 0) {
fprintf(stderr, "Error: Invalid archive (missing NEOPIG magic)\n");
fprintf(stderr, "This executable must be created with make-executable.sh\n");
return -1;
}
/* Parse hex offset */
long offset = strtol(trailer + MAGIC_LEN, NULL, 16);
return offset;
}
/* Create directory recursively */
static int mkdir_p(const char *path) {
char tmp[4096];
char *p = NULL;
size_t len;
snprintf(tmp, sizeof(tmp), "%s", path);
len = strlen(tmp);
if (tmp[len - 1] == '/') tmp[len - 1] = 0;
for (p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = 0;
mkdir(tmp, 0755);
*p = '/';
}
}
return mkdir(tmp, 0755);
}
/* Extract neopig/*.py from gzipped tarball */
static char *extract_neopig(const char *self_path, long tar_offset) {
/* Create temp directory */
char *temp_dir = strdup("/tmp/neopig_XXXXXX");
if (!temp_dir) return NULL;
if (!mkdtemp(temp_dir)) {
perror("mkdtemp");
free(temp_dir);
return NULL;
}
/* Open self and seek to tarball */
FILE *self = fopen(self_path, "rb");
if (!self) {
perror("fopen self for tar");
free(temp_dir);
return NULL;
}
if (fseek(self, tar_offset, SEEK_SET) != 0) {
perror("fseek to tar");
fclose(self);
free(temp_dir);
return NULL;
}
/* Open gzip stream */
gzFile gz = gzdopen(dup(fileno(self)), "rb");
if (!gz) {
fprintf(stderr, "gzdopen failed\n");
fclose(self);
free(temp_dir);
return NULL;
}
fclose(self);
/* Read tar headers looking for neopig/*.py */
unsigned char header[512];
int found_any = 0;
while (gzread(gz, header, 512) == 512) {
/* Check for end of archive (all zeros) */
int all_zero = 1;
for (int i = 0; i < 512 && all_zero; i++) {
if (header[i] != 0) all_zero = 0;
}
if (all_zero) break;
/* Get filename (first 100 bytes) */
char filename[101];
memcpy(filename, header, 100);
filename[100] = '\0';
/* Get file size (octal, bytes 124-135) */
char size_str[13];
memcpy(size_str, header + 124, 12);
size_str[12] = '\0';
long filesize = strtol(size_str, NULL, 8);
/* Check if this is a neopig/*.py file or requirements.txt */
char *neopig_pos = strstr(filename, "/neopig/");
char *py_ext = strstr(filename, ".py");
char *req_file = strstr(filename, "/requirements.txt");
int is_neopig_py = (neopig_pos && py_ext && py_ext > neopig_pos);
int is_requirements = (req_file != NULL);
/* Debug: show neopig-related files */
if (neopig_pos || req_file) {
fprintf(stderr, " [DEBUG] Found: %s (neopig=%d, py=%d, req=%d)\n",
filename, neopig_pos != NULL, py_ext != NULL, req_file != NULL);
}
if (is_neopig_py || is_requirements) {
/* Extract this file */
char out_path[4096];
if (is_neopig_py) {
char *basename = neopig_pos + 8; /* Skip "/neopig/" */
snprintf(out_path, sizeof(out_path), "%s/%s", temp_dir, basename);
} else {
snprintf(out_path, sizeof(out_path), "%s/requirements.txt", temp_dir);
}
int fd = open(out_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) {
unsigned char buf[CHUNK_SIZE];
long remaining = filesize;
while (remaining > 0) {
int to_read = remaining > CHUNK_SIZE ? CHUNK_SIZE : remaining;
int got = gzread(gz, buf, to_read);
if (got <= 0) break;
write(fd, buf, got);
remaining -= got;
}
close(fd);
found_any = 1;
if (is_neopig_py) {
char *basename = neopig_pos + 8;
fprintf(stderr, " Extracted: %s\n", basename);
} else {
fprintf(stderr, " Extracted: requirements.txt\n");
}
/* Skip padding to 512 boundary */
int pad = (512 - (filesize % 512)) % 512;
if (pad > 0) {
gzread(gz, buf, pad);
}
} else {
/* Skip this file's content */
long blocks = (filesize + 511) / 512;
for (long i = 0; i < blocks; i++) {
gzread(gz, header, 512);
}
}
} else {
/* Skip this file's content (padded to 512 bytes) */
long blocks = (filesize + 511) / 512;
for (long i = 0; i < blocks; i++) {
if (gzread(gz, header, 512) != 512) break;
}
}
}
gzclose(gz);
if (!found_any) {
fprintf(stderr, "Error: No neopig/*.py files found in archive\n");
free(temp_dir);
return NULL;
}
return temp_dir;
}
int main(int argc, char *argv[]) {
/* Get path to self */
char self_path[4096];
ssize_t len = readlink("/proc/self/exe", self_path, sizeof(self_path) - 1);
if (len < 0) {
/* Fallback to argv[0] */
if (argv[0][0] == '/') {
strncpy(self_path, argv[0], sizeof(self_path) - 1);
} else {
char *cwd = getcwd(NULL, 0);
snprintf(self_path, sizeof(self_path), "%s/%s", cwd, argv[0]);
free(cwd);
}
} else {
self_path[len] = '\0';
}
printf("neopig self-extracting archive\n");
printf("==============================\n");
/* Read trailer to get tarball offset */
long tar_offset = read_trailer(self_path);
if (tar_offset < 0) {
return 1;
}
printf("Archive offset: %ld bytes\n", tar_offset);
/* Extract neopig directory */
printf("Extracting neopig...\n");
char *neopig_dir = extract_neopig(self_path, tar_offset);
if (!neopig_dir) {
return 1;
}
/* Build path to serp.py */
char serp_path[4096];
snprintf(serp_path, sizeof(serp_path), "%s/serp.py", neopig_dir);
/* Find python */
const char *python = find_python();
/* Create virtualenv and install deps */
char venv_path[4096];
snprintf(venv_path, sizeof(venv_path), "%s/.venv", neopig_dir);
char req_path[4096];
snprintf(req_path, sizeof(req_path), "%s/requirements.txt", neopig_dir);
/* Check if requirements.txt exists */
if (access(req_path, F_OK) == 0) {
printf("\nCreating virtualenv...\n");
char venv_cmd[4096];
snprintf(venv_cmd, sizeof(venv_cmd), "%s -m venv %s", python, venv_path);
if (system(venv_cmd) != 0) {
fprintf(stderr, "Failed to create virtualenv\n");
free(neopig_dir);
return 1;
}
printf("Installing dependencies...\n");
char pip_cmd[8192];
snprintf(pip_cmd, sizeof(pip_cmd),
"%s/.venv/bin/pip install -r %s", neopig_dir, req_path);
if (system(pip_cmd) != 0) {
fprintf(stderr, "Failed to install dependencies\n");
free(neopig_dir);
return 1;
}
/* Use venv python */
char venv_python[4096];
snprintf(venv_python, sizeof(venv_python), "%s/.venv/bin/python", neopig_dir);
printf("\nStarting server...\n");
printf("Command: %s %s %s\n\n", venv_python, serp_path, self_path);
/* Fork and exec with venv python */
pid_t pid = fork();
if (pid < 0) {
perror("fork");
free(neopig_dir);
return 1;
}
if (pid == 0) {
execl(venv_python, "python", serp_path, self_path, NULL);
perror("execl venv python");
_exit(1);
}
int status;
waitpid(pid, &status, 0);
printf("\nCleaning up %s...\n", neopig_dir);
char rm_cmd[4096];
snprintf(rm_cmd, sizeof(rm_cmd), "rm -rf %s", neopig_dir);
system(rm_cmd);
free(neopig_dir);
return WIFEXITED(status) ? WEXITSTATUS(status) : 1;
}
/* No requirements.txt - use system python */
printf("\nStarting server (no venv)...\n");
printf("Command: %s %s %s\n\n", python, serp_path, self_path);
/* Fork and exec */
pid_t pid = fork();
if (pid < 0) {
perror("fork");
free(neopig_dir);
return 1;
}
if (pid == 0) {
/* Child - exec python */
execl(python, "python3", serp_path, self_path, NULL);
perror("execl python3");
_exit(1);
}
/* Parent - wait for child */
int status;
waitpid(pid, &status, 0);
/* Cleanup temp directory */
printf("\nCleaning up %s...\n", neopig_dir);
char rm_cmd[4096];
snprintf(rm_cmd, sizeof(rm_cmd), "rm -rf %s", neopig_dir);
system(rm_cmd);
free(neopig_dir);
return WIFEXITED(status) ? WEXITSTATUS(status) : 1;
}