modified: archive.py new file: bootstrap-neopig.sh modified: bootstrap.c modified: serp.py
397 lines
12 KiB
C
397 lines
12 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 file we want to extract */
|
|
char *neopig_pos = strstr(filename, "/neopig/");
|
|
char *py_ext = strstr(filename, ".py");
|
|
char *req_file = strstr(filename, "/requirements.txt");
|
|
char *static_vendor = strstr(filename, "/neopig/static/vendor/");
|
|
char *css_ext = strstr(filename, ".css");
|
|
char *js_ext = strstr(filename, ".js");
|
|
|
|
int is_neopig_py = (neopig_pos && py_ext && py_ext > neopig_pos);
|
|
int is_requirements = (req_file != NULL);
|
|
int is_static_asset = (static_vendor && (css_ext || js_ext));
|
|
|
|
if (is_neopig_py || is_requirements || is_static_asset) {
|
|
/* Extract this file */
|
|
char out_path[4096];
|
|
if (is_static_asset) {
|
|
/* Create static/vendor/ subdirectory */
|
|
char vendor_dir[4096];
|
|
snprintf(vendor_dir, sizeof(vendor_dir), "%s/static/vendor", temp_dir);
|
|
mkdir(vendor_dir, 0755);
|
|
char static_dir[4096];
|
|
snprintf(static_dir, sizeof(static_dir), "%s/static", temp_dir);
|
|
mkdir(static_dir, 0755);
|
|
mkdir(vendor_dir, 0755);
|
|
char *basename = static_vendor + 22; /* Skip "/neopig/static/vendor/" */
|
|
snprintf(out_path, sizeof(out_path), "%s/static/vendor/%s", temp_dir, basename);
|
|
} else 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_static_asset) {
|
|
char *basename = static_vendor + 22;
|
|
printf(" Extracted: static/vendor/%s\n", basename);
|
|
} else if (is_neopig_py) {
|
|
char *basename = neopig_pos + 8;
|
|
printf(" Extracted: %s\n", basename);
|
|
} else {
|
|
printf(" 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
|
|
* IMPORTANT: Use full path as argv[0] for proper venv detection */
|
|
pid_t pid = fork();
|
|
if (pid < 0) {
|
|
perror("fork");
|
|
free(neopig_dir);
|
|
return 1;
|
|
}
|
|
if (pid == 0) {
|
|
execl(venv_python, venv_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 (use full path as argv[0]) */
|
|
execl(python, python, serp_path, self_path, NULL);
|
|
perror("execl python");
|
|
_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;
|
|
}
|