Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 26 Mar 2026 21:20:48 +0000
From:      Yuri Victorovich <yuri@FreeBSD.org>
To:        ports-committers@FreeBSD.org, dev-commits-ports-all@FreeBSD.org, dev-commits-ports-main@FreeBSD.org
Subject:   git: 0eba64bd34ce - main - misc/github-copilot-cli: Add script and binary flavors
Message-ID:  <69c5a330.31ed3.6b679361@gitrepo.freebsd.org>

index | next in thread | raw e-mail

The branch main has been updated by yuri:

URL: https://cgit.FreeBSD.org/ports/commit/?id=0eba64bd34ce2d85b62754c746b6b84248de60fa

commit 0eba64bd34ce2d85b62754c746b6b84248de60fa
Author:     Yuri Victorovich <yuri@FreeBSD.org>
AuthorDate: 2026-03-26 19:55:15 +0000
Commit:     Yuri Victorovich <yuri@FreeBSD.org>
CommitDate: 2026-03-26 21:20:44 +0000

    misc/github-copilot-cli: Add script and binary flavors
    
    script flavor (default): existing behavior with all npm dependencies
    binary flavor (-bin): C launcher binary with all JS and Node.js
    embedded in the binary, requires only node at runtime
    (no Node.js, no npm packages, no native modules)
    
    Node.js SEA (Single Executable Application) cannot be used because
    postject_find_resource() has no FreeBSD code path. The binary flavor
    instead uses a C launcher that embeds all copilot JS files as a gzip-
    compressed tar archive, extracts to ~/.cache/github-copilot-cli/v<ver>/
    on first run, and executes them with the system node.
---
 misc/github-copilot-cli/Makefile         | 131 +++++++++++++++++-----
 misc/github-copilot-cli/files/launcher.c | 187 +++++++++++++++++++++++++++++++
 misc/github-copilot-cli/pkg-plist.binary |   1 +
 3 files changed, 293 insertions(+), 26 deletions(-)

diff --git a/misc/github-copilot-cli/Makefile b/misc/github-copilot-cli/Makefile
index 9c1841342d95..944c59e1ca63 100644
--- a/misc/github-copilot-cli/Makefile
+++ b/misc/github-copilot-cli/Makefile
@@ -2,8 +2,7 @@ PORTNAME=	github-copilot-cli
 DISTVERSION=	1.0.10
 PORTEPOCH=	1
 CATEGORIES=	misc # machine-learning
-DISTFILES=	${PORTNAME}-${DISTVERSION}${EXTRACT_SUFX} \
-		${NODE_HEADERS}${EXTRACT_SUFX}
+DISTFILES=	${PORTNAME}-${DISTVERSION}${EXTRACT_SUFX}
 DIST_SUBDIR=	${PORTNAME}
 
 MAINTAINER=	yuri@FreeBSD.org
@@ -13,9 +12,28 @@ WWW=		https://github.com/github/copilot-cli
 ONLY_FOR_ARCHS=		aarch64 amd64
 ONLY_FOR_ARCHS_REASON=	binaries are installed in folders with architecture encoded in them, patches are welcome to fix this limitation
 
+FLAVORS=	script binary
+FLAVOR?=	${FLAVORS:[1]}
+script_PKGNAMESUFFIX=
+binary_PKGNAMESUFFIX=	-bin
+binary_COMMENT=	GitHub Copilot CLI - standalone binary (no npm dependencies)
+binary_PLIST=	${.CURDIR}/pkg-plist.binary
+
 FETCH_DEPENDS=	npm:www/npm \
 		jq:textproc/jq \
 		${LOCALBASE}/share/certs/ca-root-nss.crt:security/ca_root_nss
+
+WRKSRC=		${WRKDIR}/copilot-${DISTVERSION}
+
+PACKAGE_NAME=	@github/copilot
+
+DD=		${DISTDIR}/${DIST_SUBDIR}
+
+FETCH_SCRIPT=	${PORTSDIR}/Tools/scripts/npmjs-fetch-with-dependencies.sh
+
+.if ${FLAVOR} == script
+DISTFILES+=	${NODE_HEADERS}${EXTRACT_SUFX}
+
 BUILD_DEPENDS=	npm:www/npm \
 		libsecret>0:security/libsecret \
 		vips>=8.17.2:graphics/vips
@@ -25,18 +43,43 @@ RUN_DEPENDS=	libsecret>0:security/libsecret \
 
 USES=		nodejs:run pkgconfig python:build
 
-WRKSRC=		${WRKDIR}/copilot-${DISTVERSION}
+.elif ${FLAVOR} == binary
+DISTFILES+=	${NODE_HEADERS}${EXTRACT_SUFX}
 
-PACKAGE_NAME=	@github/copilot
+BUILD_DEPENDS=	npm:www/npm \
+		libsecret>0:security/libsecret \
+		vips>=8.17.2:graphics/vips
 
-NODE_HEADERS=	node-v22.19.0-headers
+# The node binary is bundled inside the port binary; its shared libraries
+# must still be present at runtime so they are listed here.
+LIB_DEPENDS=	libada.so:devel/libada \
+		libbrotlidec.so:archivers/brotli \
+		libcares.so:dns/c-ares \
+		libgtest.so:devel/googletest \
+		libhdr_histogram.so:graphics/hdr_histogram \
+		libicui18n.so:devel/icu \
+		libllhttp.so:www/llhttp \
+		libmerve.so:devel/merve \
+		libnbytes.so:www/nbytes \
+		libnghttp2.so:www/libnghttp2 \
+		libnghttp3.so:www/libnghttp3 \
+		libngtcp2.so:net/libngtcp2 \
+		libsimdjson.so:devel/simdjson \
+		libsimdutf.so:converters/simdutf \
+		libsqlite3.so:databases/sqlite3 \
+		libuv.so:devel/libuv \
+		libuvwasi.so:devel/uvwasi \
+		libzstd.so:archivers/zstd
+RUN_DEPENDS=	libsecret>0:security/libsecret \
+		vips>=8.17.2:graphics/vips
 
-JS_ARCH=	${ARCH:S/amd64/x64/:S/aarch64/arm64/}
-PLIST_SUB=	JS_ARCH=${JS_ARCH}
+USES=		nodejs:build pkgconfig python:build
 
-DD=		${DISTDIR}/${DIST_SUBDIR}
+.endif	# FLAVOR
 
-FETCH_SCRIPT=	${PORTSDIR}/Tools/scripts/npmjs-fetch-with-dependencies.sh
+NODE_HEADERS=	node-v22.19.0-headers
+
+JS_ARCH=	${ARCH:S/amd64/x64/:S/aarch64/arm64/}
 
 DEP_MODULES=			pty sharp keytar node_addon_api
 dep_pty_npm_name=		@devm33/node-pty
@@ -52,41 +95,49 @@ dep_node_addon_api_version=	8.5.0
 DISTFILES+=	${dep:S/_/-/g}-${dep_${dep}_version}${EXTRACT_SUFX}
 .endfor
 
+PLIST_SUB=	JS_ARCH=${JS_ARCH}
+
 do-fetch:
-	@if ! [ -f ${DD}/${PORTNAME}-${DISTVERSION}${EXTRACT_SUFX} ] || \
-	    ! [ -f ${DD}/${NODE_HEADERS}${EXTRACT_SUFX} ] || \
-	    ! [ -f ${DD}/pty-${dep_pty_version}${EXTRACT_SUFX} ] || \
-	    ! [ -f ${DD}/sharp-${dep_sharp_version}${EXTRACT_SUFX} ] || \
-	    ! [ -f ${DD}/keytar-${dep_keytar_version}${EXTRACT_SUFX} ] || \
-	    ! [ -f ${DD}/node-addon-api-${dep_node_addon_api_version}${EXTRACT_SUFX} ]; then \
-		${MKDIR} ${DD} && \
+	@${MKDIR} ${DD}
+	@if ! [ -f ${DD}/${PORTNAME}-${DISTVERSION}${EXTRACT_SUFX} ]; then \
+		${ECHO} "====> Fetching ${PORTNAME}-${DISTVERSION}${EXTRACT_SUFX}" && \
+		${SETENV} TMPDIR=${WRKDIR} LOCALBASE=${LOCALBASE} ${FETCH_SCRIPT} \
+			${PACKAGE_NAME} ${DISTVERSION} \
+			${FILESDIR}/package-lock.json \
+			${DD}/${PORTNAME}-${DISTVERSION}${EXTRACT_SUFX}; \
+	fi
+	@if ! [ -f ${DD}/${NODE_HEADERS}${EXTRACT_SUFX} ]; then \
 		${ECHO} "====> Fetching ${NODE_HEADERS}${EXTRACT_SUFX}" && \
-		${FETCH_CMD} -q https://nodejs.org/download/release/v22.19.0/${NODE_HEADERS}${EXTRACT_SUFX} -o ${DD}/${NODE_HEADERS}${EXTRACT_SUFX} && \
+		${FETCH_CMD} -q https://nodejs.org/download/release/v22.19.0/${NODE_HEADERS}${EXTRACT_SUFX} \
+			-o ${DD}/${NODE_HEADERS}${EXTRACT_SUFX}; \
+	fi
+	@if ! [ -f ${DD}/pty-${dep_pty_version}${EXTRACT_SUFX} ]; then \
 		${ECHO} "====> Fetching dependency pty" && \
 		${SETENV} TMPDIR=${WRKDIR} LOCALBASE=${LOCALBASE} ${FETCH_SCRIPT} \
 			${dep_pty_npm_name} ${dep_pty_version} \
 			${FILESDIR}/package-lock-pty.json \
-			${DD}/pty-${dep_pty_version}${EXTRACT_SUFX} && \
+			${DD}/pty-${dep_pty_version}${EXTRACT_SUFX}; \
+	fi
+	@if ! [ -f ${DD}/sharp-${dep_sharp_version}${EXTRACT_SUFX} ]; then \
 		${ECHO} "====> Fetching dependency sharp" && \
 		${SETENV} TMPDIR=${WRKDIR} LOCALBASE=${LOCALBASE} ${FETCH_SCRIPT} \
 			${dep_sharp_npm_name} ${dep_sharp_version} \
 			${FILESDIR}/package-lock-sharp.json \
-			${DD}/sharp-${dep_sharp_version}${EXTRACT_SUFX} && \
+			${DD}/sharp-${dep_sharp_version}${EXTRACT_SUFX}; \
+	fi
+	@if ! [ -f ${DD}/keytar-${dep_keytar_version}${EXTRACT_SUFX} ]; then \
 		${ECHO} "====> Fetching dependency keytar" && \
 		${SETENV} TMPDIR=${WRKDIR} LOCALBASE=${LOCALBASE} ${FETCH_SCRIPT} \
 			${dep_keytar_npm_name} ${dep_keytar_version} \
 			${FILESDIR}/package-lock-keytar.json \
-			${DD}/keytar-${dep_keytar_version}${EXTRACT_SUFX} && \
+			${DD}/keytar-${dep_keytar_version}${EXTRACT_SUFX}; \
+	fi
+	@if ! [ -f ${DD}/node-addon-api-${dep_node_addon_api_version}${EXTRACT_SUFX} ]; then \
 		${ECHO} "====> Fetching dependency node-addon-api" && \
 		${SETENV} TMPDIR=${WRKDIR} LOCALBASE=${LOCALBASE} ${FETCH_SCRIPT} \
 			${dep_node_addon_api_npm_name} ${dep_node_addon_api_version} \
 			${FILESDIR}/package-lock-node-addon-api.json \
-			${DD}/node-addon-api-${dep_node_addon_api_version}${EXTRACT_SUFX} && \
-		${ECHO} "====> Fetching ${PORTNAME}-${DISTVERSION}${EXTRACT_SUFX}" && \
-		${SETENV} TMPDIR=${WRKDIR} LOCALBASE=${LOCALBASE} ${FETCH_SCRIPT} \
-			${PACKAGE_NAME} ${DISTVERSION} \
-			${FILESDIR}/package-lock.json \
-			${DD}/${PORTNAME}-${DISTVERSION}${EXTRACT_SUFX}; \
+			${DD}/node-addon-api-${dep_node_addon_api_version}${EXTRACT_SUFX}; \
 	fi
 
 post-extract:
@@ -121,8 +172,33 @@ do-build:
 		${SETENV} HOME=${WRKDIR} CFLAGS="-I${LOCALBASE}/include" CXXFLAGS="-I${LOCALBASE}/include" \
 			npm rebuild --nodedir=${LOCALBASE} && \
 		${CP} build/Release/keytar.node ${WRKSRC}/node_modules/${PACKAGE_NAME}/prebuilds/freebsd-x64/
+.if ${FLAVOR} == binary
+	@${ECHO_MSG} "====> Creating copilot bundle (includes node runtime)..."
+	# Copy @img/sharp-freebsd-x64 into copilot's node_modules so it is findable
+	# after extraction to the cache dir (node resolves it relative to app.js)
+	@${MKDIR} ${WRKSRC}/node_modules/${PACKAGE_NAME}/node_modules/@img
+	@${CP} -r ${WRKSRC}/node_modules/@img/sharp-freebsd-x64 \
+		${WRKSRC}/node_modules/${PACKAGE_NAME}/node_modules/@img/
+	# Embed the node runtime so it runs without a system node installation
+	@${CP} ${LOCALBASE}/bin/node ${WRKSRC}/node_modules/${PACKAGE_NAME}/node
+	@cd ${WRKSRC}/node_modules/${PACKAGE_NAME} && \
+		${TAR} --exclude=./ripgrep --exclude=./sharp \
+			--exclude=./changelog.json --exclude=./npm-loader.js.orig \
+			-cJf ${WRKSRC}/copilot_bundle.txz .
+	@${ECHO_MSG} "====> Building copilot launcher..."
+	@${PRINTF} '\t.global _binary_copilot_bundle_txz_start\n\t.global _binary_copilot_bundle_txz_end\n_binary_copilot_bundle_txz_start:\n\t.incbin "%s"\n_binary_copilot_bundle_txz_end:\n' \
+		"${WRKSRC}/copilot_bundle.txz" > ${WRKSRC}/blob.s
+	@${CC} -c ${WRKSRC}/blob.s -o ${WRKSRC}/blob.o
+	@${CC} ${CFLAGS} \
+		-DPREFIX='"${PREFIX}"' \
+		-DPORTVERSION='"${PORTVERSION}"' \
+		-c ${FILESDIR}/launcher.c -o ${WRKSRC}/launcher.o
+	@${CC} ${LDFLAGS} -o ${WRKSRC}/copilot ${WRKSRC}/launcher.o ${WRKSRC}/blob.o
+	@${STRIP_CMD} ${WRKSRC}/copilot
+.endif	# FLAVOR == binary
 
 do-install:
+.if ${FLAVOR} == script
 	# install files
 	cd ${WRKSRC} && \
 		${COPYTREE_SHARE} . ${STAGEDIR}${PREFIX}/lib
@@ -148,5 +224,8 @@ do-install:
 	@${RLN} -s ${STAGEDIR}${PREFIX}/lib/node_modules/.bin/copilot ${STAGEDIR}${PREFIX}/bin/copilot
 	# strip binaries
 	@${FIND} ${STAGEDIR}${PREFIX}/lib/node_modules/${PACKAGE_NAME} -path "*/build/*" -name *.node | ${XARGS} ${STRIP_CMD}
+.elif ${FLAVOR} == binary
+	${INSTALL_PROGRAM} ${WRKSRC}/copilot ${STAGEDIR}${PREFIX}/bin/copilot
+.endif	# FLAVOR
 
 .include <bsd.port.mk>
diff --git a/misc/github-copilot-cli/files/launcher.c b/misc/github-copilot-cli/files/launcher.c
new file mode 100644
index 000000000000..ee3104f1a5cd
--- /dev/null
+++ b/misc/github-copilot-cli/files/launcher.c
@@ -0,0 +1,187 @@
+/*
+ * launcher.c - GitHub Copilot CLI binary flavor launcher
+ *
+ * Extracts the bundled copilot JS files and node runtime to a version-specific
+ * cache directory and runs them. The bundle (xz-compressed tar) contains the
+ * full node binary so no system node installation is required.
+ *
+ * Node.js SEA (Single Executable Application) cannot be used because
+ * postject_find_resource() has no FreeBSD code path.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#ifndef PORTVERSION
+#define PORTVERSION "unknown"
+#endif
+
+#define CACHE_SUBDIR "github-copilot-cli/v" PORTVERSION
+
+extern const char _binary_copilot_bundle_txz_start[];
+extern const char _binary_copilot_bundle_txz_end[];
+
+static int
+makedirs(const char *path)
+{
+	char buf[1024];
+	char *p;
+	struct stat st;
+
+	if (stat(path, &st) == 0)
+		return (0);
+
+	snprintf(buf, sizeof(buf), "%s", path);
+	for (p = buf + 1; *p != '\0'; p++) {
+		if (*p == '/') {
+			*p = '\0';
+			if (stat(buf, &st) != 0 && mkdir(buf, 0755) != 0 &&
+			    errno != EEXIST)
+				return (-1);
+			*p = '/';
+		}
+	}
+	return (mkdir(buf, 0755) == 0 || errno == EEXIST ? 0 : -1);
+}
+
+static int
+extract_bundle(const char *destdir)
+{
+	char tmpdir[1024], archpath[1024];
+	const char *data;
+	size_t size;
+	FILE *f;
+	pid_t pid;
+	int status;
+
+	/* Write bundle to a temp archive file */
+	snprintf(archpath, sizeof(archpath), "%s/.bundle.txz.tmp", destdir);
+	data = _binary_copilot_bundle_txz_start;
+	size = (size_t)(_binary_copilot_bundle_txz_end -
+	    _binary_copilot_bundle_txz_start);
+
+	f = fopen(archpath, "wb");
+	if (f == NULL) {
+		fprintf(stderr, "copilot: cannot write bundle: %s\n",
+		    strerror(errno));
+		return (-1);
+	}
+	if (fwrite(data, 1, size, f) != size) {
+		fclose(f);
+		unlink(archpath);
+		fprintf(stderr, "copilot: bundle write failed\n");
+		return (-1);
+	}
+	fclose(f);
+
+	/* Extract into a temp subdir, then atomically rename */
+	snprintf(tmpdir, sizeof(tmpdir), "%s.extracting", destdir);
+	makedirs(tmpdir);
+
+	pid = fork();
+	if (pid < 0) {
+		unlink(archpath);
+		return (-1);
+	}
+	if (pid == 0) {
+		execl("/usr/bin/tar", "tar", "-xJf", archpath, "-C", tmpdir,
+		    (char *)NULL);
+		_exit(127);
+	}
+	waitpid(pid, &status, 0);
+	unlink(archpath);
+
+	if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+		fprintf(stderr, "copilot: bundle extraction failed\n");
+		/* cleanup tmpdir */
+		pid = fork();
+		if (pid == 0) {
+			execl("/bin/rm", "rm", "-rf", tmpdir, (char *)NULL);
+			_exit(1);
+		}
+		if (pid > 0)
+			waitpid(pid, NULL, 0);
+		return (-1);
+	}
+
+	/* Atomic rename: tmpdir -> destdir */
+	if (rename(tmpdir, destdir) != 0) {
+		/* Another process may have already created destdir - that's ok */
+		if (errno != EEXIST && errno != ENOTEMPTY) {
+			fprintf(stderr, "copilot: rename failed: %s\n",
+			    strerror(errno));
+			return (-1);
+		}
+		/* Use existing destdir; clean up tmpdir */
+		pid = fork();
+		if (pid == 0) {
+			execl("/bin/rm", "rm", "-rf", tmpdir, (char *)NULL);
+			_exit(1);
+		}
+		if (pid > 0)
+			waitpid(pid, NULL, 0);
+	}
+
+	return (0);
+}
+
+int
+main(int argc, char *argv[])
+{
+	const char *xdg, *home;
+	char cache_dir[1024], node_path[1080], script_path[1080];
+	char **new_argv;
+	struct stat st;
+	int i;
+
+	/* Determine cache directory */
+	xdg = getenv("XDG_CACHE_HOME");
+	home = getenv("HOME");
+	if (xdg != NULL && xdg[0] != '\0')
+		snprintf(cache_dir, sizeof(cache_dir), "%s/" CACHE_SUBDIR, xdg);
+	else if (home != NULL && home[0] != '\0')
+		snprintf(cache_dir, sizeof(cache_dir),
+		    "%s/.cache/" CACHE_SUBDIR, home);
+	else {
+		fprintf(stderr,
+		    "copilot: HOME or XDG_CACHE_HOME must be set\n");
+		return (1);
+	}
+
+	snprintf(node_path, sizeof(node_path), "%s/node", cache_dir);
+	snprintf(script_path, sizeof(script_path), "%s/index.js", cache_dir);
+
+	/* Extract bundle if not already done (check for node binary) */
+	if (stat(node_path, &st) != 0) {
+		if (makedirs(cache_dir) != 0) {
+			fprintf(stderr, "copilot: cannot create %s: %s\n",
+			    cache_dir, strerror(errno));
+			return (1);
+		}
+		if (extract_bundle(cache_dir) != 0)
+			return (1);
+	}
+
+	/* Build argument vector for the bundled node */
+	new_argv = malloc((size_t)(argc + 2) * sizeof(char *));
+	if (new_argv == NULL) {
+		fprintf(stderr, "copilot: malloc failed\n");
+		return (1);
+	}
+	new_argv[0] = node_path;
+	new_argv[1] = script_path;
+	for (i = 1; i < argc; i++)
+		new_argv[i + 1] = argv[i];
+	new_argv[argc + 1] = NULL;
+
+	execv(node_path, new_argv);
+
+	fprintf(stderr, "copilot: cannot exec %s: %s\n", node_path,
+	    strerror(errno));
+	return (1);
+}
diff --git a/misc/github-copilot-cli/pkg-plist.binary b/misc/github-copilot-cli/pkg-plist.binary
new file mode 100644
index 000000000000..a2a43e65b9fc
--- /dev/null
+++ b/misc/github-copilot-cli/pkg-plist.binary
@@ -0,0 +1 @@
+bin/copilot


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?69c5a330.31ed3.6b679361>