aboutsummaryrefslogtreecommitdiff
path: root/misc/opencode/Makefile
blob: 277178c23e76da0f5934565077240c137e6bf0e6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
PORTNAME=	opencode
DISTVERSIONPREFIX=	v
DISTVERSION=	2.2.5
CATEGORIES=	misc

# Upstream ships no FreeBSD builds of OpenTUI's Zig core, fff's Rust cdylib, the
# opencode-pty terminal daemon or the web UI's native Vite 8 bindings, so the
# port builds them from source (extra source trees plus a WASM fallback) and
# embeds everything into a single `bun --compile` executable.  See
# files/setup-*-freebsd.py, files/*.patch and files/webui-wasm-package.json.
#
# OPENTUI_VERSION and FFF_VERSION must match the pinned @opentui/core and
# @ff-labs/fff-bun releases (the root package.json catalog and
# packages/core/package.json) or the native FFI ABI breaks.
OPENTUI_VERSION=	0.8.0
FFF_VERSION=	0.10.5
#
# opencode-pty is the persistent terminal daemon the client spawns.  It is built
# from the same source tree as the client (packages/process-daemon), and
# GHOSTTY_REVISION must match its ghostty-revision file, or the client rejects
# the daemon as a protocol mismatch.
GHOSTTY_REVISION=	ab0b9da9e88fcb4b0533a1854e84628f663930af

MASTER_SITES=	https://registry.npmjs.org/@ff-labs/fff-bun/-/:fffbun
DISTFILES=	fff-bun-${FFF_VERSION}.tgz:fffbun
DIST_SUBDIR=	${PORTNAME}

USE_GITHUB=	yes
GH_ACCOUNT=	anomalyco
GH_PROJECT=	opencode
GH_TAGNAME=	v${DISTVERSION}
GH_TUPLE=	anomalyco:opentui:v${OPENTUI_VERSION}:opentui \
		dmtrKovalenko:fff:v${FFF_VERSION}:fff \
		ghostty-org:ghostty:${GHOSTTY_REVISION}:ghostty

MAINTAINER=	samm@FreeBSD.org
COMMENT=	AI coding agent for the terminal
WWW=		https://opencode.ai/ \
		https://github.com/anomalyco/opencode

LICENSE=	MIT

ONLY_FOR_ARCHS=		aarch64 amd64
ONLY_FOR_ARCHS_REASON=	OpenTUI's Zig core is only wired up for FreeBSD/aarch64 and FreeBSD/amd64

USES=		python:build cargo

# The port drives the whole build; keep cargo.mk from adding its own
# do-build/do-install/do-test.
CARGO_BUILD=	no
CARGO_INSTALL=	no
CARGO_TEST=	no

# node (husky `prepare` + Vite 8 web UI).  Use node24: on aarch64 Vite 8's
# rolldown has no native binding and its WASI fallback SIGSEGVs under node26.
BUILD_DEPENDS=	${LOCALBASE}/bin/bun:lang/bun \
		${LOCALBASE}/bin/zig:lang/zig \
		${LOCALBASE}/bin/node:www/node24 \
		npm-node24>0:www/npm-node24 \
		gmake:devel/gmake
# post-fetch warms the offline bun and Ghostty Zig caches, so both are needed.
# Zig 0.16 hardcodes /etc/ssl/cert.pem on FreeBSD; ca_root_nss provides it.
FETCH_DEPENDS=	${LOCALBASE}/bin/bun:lang/bun \
		${LOCALBASE}/bin/zig:lang/zig \
		${LOCALBASE}/share/certs/ca-root-nss.crt:security/ca_root_nss

# OpenCode's bundled ripgrep bootstrap has no FreeBSD build and only falls back
# to a system `rg`, so make sure one exists: it backs the non-VCS file search
# and the grep/glob tools.
RUN_DEPENDS=	rg:textproc/ripgrep

OPENTUI_WRKSRC=	${WRKDIR}/opentui-${OPENTUI_VERSION}
FFF_WRKSRC=	${WRKDIR}/fff-${FFF_VERSION}
OPENCODE_PTY_WRKSRC=	${WRKSRC}/packages/process-daemon
GHOSTTY_WRKSRC=		${WRKDIR}/ghostty-${GHOSTTY_REVISION}

# Build only the fff C-ABI cdylib (not the nvim/python/mcp crates).
CARGO_BUILD_ARGS=	--package fff-c
FFF_SO=			${CARGO_TARGET_DIR}/release/libfff_c.so

BUN=			${LOCALBASE}/bin/bun
ZIG=			${LOCALBASE}/bin/zig
NODE=			${LOCALBASE}/bin/node
NODE_GYP=		${LOCALBASE}/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
BUN_INSTALL_CACHE_DIR=	${WRKDIR}/bun-cache
WEBUI_WASM_DIR=	${WRKDIR}/gen-webui
WEBUI_WASM_CACHE=	${DISTDIR}/${DIST_SUBDIR}/opencode-webui-wasm-${DISTVERSION}.tar.gz
BUN_CACHE_TARBALL=	${DISTDIR}/${DIST_SUBDIR}/opencode-bun-cache-${DISTVERSION}.tar.gz
ZIG_PTY_CACHE_DIR=	${WRKDIR}/zig-pty-cache
ZIG_PTY_CACHE=		${DISTDIR}/${DIST_SUBDIR}/opencode-pty-zig-cache-${DISTVERSION}.tar.gz

PLIST_FILES=	bin/${PORTNAME} \
		lib/opencode/watcher.node

.include <bsd.port.pre.mk>

# cargo.mk hard-assigns CARGO_WRKSRC from WRKSRC, so point it at the fff tree.
CARGO_WRKSRC=	${FFF_WRKSRC}

# OpenTUI/Bun arch names differ from ARCH: x64/arm64 for the JS side, and
# x86_64-freebsd/aarch64-freebsd for the Zig artifact directory.
.if ${ARCH} == amd64
OPENTUI_ARCH=	x64
OPENTUI_ZIGDIR=	x86_64-freebsd
.elif ${ARCH} == aarch64
OPENTUI_ARCH=	arm64
OPENTUI_ZIGDIR=	aarch64-freebsd
.endif

OPENTUI_SO=		${OPENTUI_WRKSRC}/packages/native/lib/${OPENTUI_ZIGDIR}/libopentui.so
OPENCODE_BIN=		${WRKSRC}/packages/cli/dist/cli-freebsd-${OPENTUI_ARCH}/bin/opencode
# Built from source by this port and embedded into the executable (see do-build).
OPENCODE_PTY_BIN=	${CARGO_TARGET_DIR}/release/opencode-pty

# Build the offline caches at fetch time.  The generated archives live in
# DISTDIR but are deliberately not listed in DISTFILES, so `make checksum` does
# not try to verify the generated files.
post-fetch:
	@${ECHO_MSG} "===> Warming bun install cache"
	@${RM} -rf ${WRKDIR}/gen && ${MKDIR} ${WRKDIR}/gen
	@${TAR} xf ${DISTDIR}/${DIST_SUBDIR}/${DISTFILE_DEFAULT} -C ${WRKDIR}/gen
	@if [ ! -f ${BUN_CACHE_TARBALL} ]; then \
		cd ${WRKDIR}/gen/opencode-${DISTVERSION} && \
		${SETENV} BUN_INSTALL_CACHE_DIR=${BUN_INSTALL_CACHE_DIR} \
			${BUN} install --frozen-lockfile && \
		${TAR} czf ${BUN_CACHE_TARBALL} -C ${BUN_INSTALL_CACHE_DIR} . ; \
	fi
	@if [ ! -f ${WEBUI_WASM_CACHE} ]; then \
		${ECHO_MSG} "===> Staging Web UI WASM fallbacks"; \
		${RM} -rf ${WEBUI_WASM_DIR} && ${MKDIR} ${WEBUI_WASM_DIR} && \
		${CP} ${FILESDIR}/webui-wasm-package.json ${WEBUI_WASM_DIR}/package.json && \
		cd ${WEBUI_WASM_DIR} && \
		${SETENV} BUN_INSTALL_CACHE_DIR=${BUN_INSTALL_CACHE_DIR} ${BUN} install && \
		${TAR} czf ${WEBUI_WASM_CACHE} -C ${WEBUI_WASM_DIR} node_modules ; \
	fi
	@if [ ! -f ${ZIG_PTY_CACHE} ]; then \
		${ECHO_MSG} "===> Warming the Ghostty Zig dependency cache"; \
		${RM} -rf ${WRKDIR}/gen-ghostty && ${MKDIR} ${WRKDIR}/gen-ghostty && \
		${TAR} xf ${DISTDIR}/${DIST_SUBDIR}/${DISTFILE_ghostty} \
			-C ${WRKDIR}/gen-ghostty && \
		cd ${WRKDIR}/gen-ghostty/ghostty-${GHOSTTY_REVISION} && \
		${SETENV} ZIG_GLOBAL_CACHE_DIR=${ZIG_PTY_CACHE_DIR} \
			${ZIG} build --fetch=all && \
		${TAR} czf ${ZIG_PTY_CACHE} -C ${ZIG_PTY_CACHE_DIR} . ; \
	fi

post-extract:
	@${MKDIR} ${BUN_INSTALL_CACHE_DIR}
	@${TAR} xf ${BUN_CACHE_TARBALL} -C ${BUN_INSTALL_CACHE_DIR}
	@${MKDIR} ${ZIG_PTY_CACHE_DIR}
	@${TAR} xf ${ZIG_PTY_CACHE} -C ${ZIG_PTY_CACHE_DIR}

# OpenTUI and Ghostty are extra source trees, and opencode-pty lives in
# packages/process-daemon, so their patches are applied by hand.
post-patch:
	@${PATCH} -d ${OPENTUI_WRKSRC} -p0 --batch < ${FILESDIR}/opentui-build.zig.patch
	@${PATCH} -d ${OPENTUI_WRKSRC} -p0 --batch < ${FILESDIR}/opentui-ghostty-vt-freebsd.patch
	@${PATCH} -d ${OPENCODE_PTY_WRKSRC} -p1 --batch < ${FILESDIR}/opencode-pty-build.rs.patch
	@${PATCH} -d ${GHOSTTY_WRKSRC} -p1 --batch < ${FILESDIR}/ghostty-zig-0.16-std.patch

do-build:
	@${ECHO_MSG} "===> Installing OpenCode dependencies (offline)"
	@cd ${WRKSRC} && ${SETENV} BUN_INSTALL_CACHE_DIR=${BUN_INSTALL_CACHE_DIR} \
		${BUN} install --frozen-lockfile
	@${ECHO_MSG} "===> Building the @parcel/watcher kqueue backend"
	@for d in ${WRKSRC}/node_modules/.bun/@parcel+watcher@*/node_modules/@parcel/watcher; do \
		[ -d "$${d}" ] || continue; \
		(cd "$${d}" && ${SETENV} HOME=${WRKDIR} ${MAKE_ENV} \
			${NODE} ${NODE_GYP} rebuild --nodedir=${LOCALBASE} --python=${PYTHON_CMD}); \
	done
	@${ECHO_MSG} "===> Installing @ff-labs/fff-bun (upstream ships no FreeBSD package)"
	@${MKDIR} ${WRKSRC}/node_modules/@ff-labs
	@${RM} -rf ${WRKSRC}/node_modules/@ff-labs/fff-bun
	@${MKDIR} ${WRKSRC}/node_modules/@ff-labs/fff-bun
	@${TAR} xf ${DISTDIR}/${DIST_SUBDIR}/fff-bun-${FFF_VERSION}.tgz \
		-C ${WRKSRC}/node_modules/@ff-labs/fff-bun --strip-components=1
	@${ECHO_MSG} "===> Adding Web UI WASM fallbacks"
	@${TAR} xf ${WEBUI_WASM_CACHE} -C ${WRKSRC}
	@${MKDIR} ${WRKSRC}/node_modules/lightningcss-freebsd-arm64
	@${ECHO_CMD} '{"name":"lightningcss-freebsd-arm64","version":"1.32.0","main":"index.js"}' \
		> ${WRKSRC}/node_modules/lightningcss-freebsd-arm64/package.json
	@${ECHO_CMD} 'module.exports = require("lightningcss-wasm")' \
		> ${WRKSRC}/node_modules/lightningcss-freebsd-arm64/index.js
	@${ECHO_MSG} "===> Building OpenTUI native core"
	@cd ${OPENTUI_WRKSRC}/packages/native && \
		${SETENV} HOME=${WRKDIR} \
			ZIG_GLOBAL_CACHE_DIR=${WRKDIR}/zig-cache \
			ZIG_LOCAL_CACHE_DIR=${WRKDIR}/zig-cache-local \
			${SH} scripts/prepare-zig-deps.sh && \
		${SETENV} HOME=${WRKDIR} \
			ZIG_GLOBAL_CACHE_DIR=${WRKDIR}/zig-cache \
			ZIG_LOCAL_CACHE_DIR=${WRKDIR}/zig-cache-local \
			${ZIG} build -Doptimize=ReleaseFast
	@${ECHO_MSG} "===> Wiring OpenTUI for FreeBSD"
	@cd ${WRKSRC} && ${PYTHON_CMD} ${FILESDIR}/setup-opentui-freebsd.py \
		"${WRKSRC}" "${OPENTUI_SO}" "${OPENTUI_VERSION}" "${OPENTUI_ARCH}"
	@${ECHO_MSG} "===> Building the fff native library"
	@${CARGO_CARGO_RUN} build --manifest-path ${CARGO_CARGOTOML} ${CARGO_BUILD_ARGS}
	@${ECHO_MSG} "===> Wiring fff for FreeBSD"
	@cd ${WRKSRC} && ${PYTHON_CMD} ${FILESDIR}/setup-fff-freebsd.py \
		"${WRKSRC}" "${FFF_SO}" "${FFF_VERSION}" "${OPENTUI_ARCH}"
	@${ECHO_MSG} "===> Building the opencode-pty terminal daemon"
	@cd ${OPENCODE_PTY_WRKSRC} && ${SETENV} ${CARGO_ENV} \
		HOME=${WRKDIR} \
		ZIG_GLOBAL_CACHE_DIR=${ZIG_PTY_CACHE_DIR} \
		GHOSTTY_SOURCE_DIR=${GHOSTTY_WRKSRC} \
		OPENCODE_ZIG_CPU=baseline \
		${CARGO} build --release --locked
	@test -x ${OPENCODE_PTY_BIN} || (${ECHO_MSG} "Missing ${OPENCODE_PTY_BIN}"; ${FALSE})
	@${ECHO_MSG} "===> Building the web UI and compiling the OpenCode binary"
	@cd ${WRKSRC}/packages/cli && ${SETENV} \
		HOME=${WRKDIR} \
		OPENCODE_VERSION=${DISTVERSION} \
		OPENCODE_TARGET=freebsd-${OPENTUI_ARCH} \
		BUN_INSTALL_CACHE_DIR=${BUN_INSTALL_CACHE_DIR} \
		OPENCODE_PTY_BIN=${OPENCODE_PTY_BIN} \
		${BUN} run script/build.ts \
			--skip-install \
			--outdir=dist
	@test -x ${OPENCODE_BIN} || (${ECHO_MSG} "Missing ${OPENCODE_BIN}"; ${FALSE})

do-install:
	# bun --compile appends the standalone payload to the executable.  Stripping
	# it (the default ${INSTALL_PROGRAM}) discards that payload, leaving a binary
	# that runs as plain bun, so install it unstripped.
	${INSTALL} -m 555 ${OPENCODE_BIN} ${STAGEDIR}${PREFIX}/bin/opencode
	@${MKDIR} ${STAGEDIR}${PREFIX}/lib/opencode
	@for f in ${WRKSRC}/node_modules/.bun/@parcel+watcher@*/node_modules/@parcel/watcher/build/Release/watcher.node; do \
		[ -f "$${f}" ] || continue; \
		${INSTALL} -m 555 "$${f}" ${STAGEDIR}${PREFIX}/lib/opencode/watcher.node; \
	done

do-test:
	@${ECHO_MSG} "==> Running OpenCode smoke test..."
	${STAGEDIR}${PREFIX}/bin/opencode --version
	@${ECHO_MSG} "==> Checking the fff native library is embedded..."
	@if ! ${GREP} -aq "crates/fff-c/src" ${STAGEDIR}${PREFIX}/bin/opencode; then \
		${ECHO_MSG} "ERROR: the fff native library is not embedded in the binary"; \
		${FALSE}; \
	fi
	@${ECHO_MSG} "==> Checking the opencode-pty terminal daemon is embedded..."
	@if ! ${GREP} -aq "libghostty" ${STAGEDIR}${PREFIX}/bin/opencode; then \
		${ECHO_MSG} "ERROR: the opencode-pty terminal daemon is not embedded in the binary"; \
		${FALSE}; \
	fi
	@${ECHO_MSG} "==> Checking the @parcel/watcher native binding is installed..."
	@test -f ${STAGEDIR}${PREFIX}/lib/opencode/watcher.node || \
		(${ECHO_MSG} "ERROR: watcher.node is missing"; ${FALSE})

.include <bsd.port.post.mk>