summaryrefslogtreecommitdiffstats
path: root/classes/go-mod-discovery.bbclass
diff options
context:
space:
mode:
Diffstat (limited to 'classes/go-mod-discovery.bbclass')
-rw-r--r--classes/go-mod-discovery.bbclass441
1 files changed, 441 insertions, 0 deletions
diff --git a/classes/go-mod-discovery.bbclass b/classes/go-mod-discovery.bbclass
new file mode 100644
index 00000000..0d703d0a
--- /dev/null
+++ b/classes/go-mod-discovery.bbclass
@@ -0,0 +1,441 @@
1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7# go-mod-discovery.bbclass
8#
9# Provides a do_discover_modules task for Go projects that downloads complete
10# module metadata from proxy.golang.org for use with the bootstrap strategy.
11#
12# USAGE:
13# 1. Add to recipe: inherit go-mod-discovery
14# 2. Set required variables (see CONFIGURATION below)
15# 3. Run discovery: bitbake <recipe> -c discover_modules
16# (This automatically: downloads modules, extracts metadata, regenerates recipe)
17# 4. Build normally: bitbake <recipe>
18#
19# CONFIGURATION:
20#
21# Required (must be set by recipe):
22#
23# GO_MOD_DISCOVERY_BUILD_TARGET - Build target for go build
24# Example: "./cmd/server" or "./..."
25#
26# Optional (have sensible defaults):
27#
28# GO_MOD_DISCOVERY_SRCDIR - Directory containing go.mod
29# Default: "${S}/src/import" (standard Go recipe layout)
30#
31# GO_MOD_DISCOVERY_BUILD_TAGS - Build tags for go build
32# Default: "${TAGS}" (uses recipe's TAGS variable if set)
33# Example: "netcgo osusergo static_build"
34#
35# GO_MOD_DISCOVERY_LDFLAGS - Linker flags for go build
36# Default: "-w -s"
37# Example: "-X main.version=${PV} -w -s"
38#
39# GO_MOD_DISCOVERY_GOPATH - GOPATH for discovery build
40# Default: "${S}/src/import/.gopath:${S}/src/import/vendor"
41#
42# GO_MOD_DISCOVERY_OUTPUT - Output binary path
43# Default: "${WORKDIR}/discovery-build-output"
44#
45# GO_MOD_DISCOVERY_DIR - Persistent cache location
46# Default: "${TOPDIR}/go-mod-discovery/${PN}/${PV}"
47#
48# GO_MOD_DISCOVERY_MODULES_JSON - Output path for extracted module metadata
49# Default: "${GO_MOD_DISCOVERY_DIR}/modules.json"
50#
51# GO_MOD_DISCOVERY_SKIP_EXTRACT - Set to "1" to skip automatic extraction
52# Default: "0" (extraction runs automatically)
53#
54# GO_MOD_DISCOVERY_SKIP_GENERATE - Set to "1" to skip automatic recipe generation
55# Default: "0" (generation runs automatically)
56#
57# GO_MOD_DISCOVERY_GIT_REPO - Git repository URL for recipe generation
58# Example: "https://github.com/rancher/k3s.git"
59# Required for automatic generation
60#
61# GO_MOD_DISCOVERY_GIT_REF - Git ref (commit/tag) for recipe generation
62# Default: "${SRCREV}" (uses recipe's SRCREV)
63#
64# GO_MOD_DISCOVERY_RECIPEDIR - Output directory for generated .inc files
65# Default: "${FILE_DIRNAME}" (recipe's directory)
66#
67# MINIMAL EXAMPLE (manual generation - no GIT_REPO set):
68#
69# TAGS = "netcgo osusergo"
70# GO_MOD_DISCOVERY_BUILD_TARGET = "./cmd/myapp"
71# inherit go-mod-discovery
72# # Run: bitbake myapp -c discover_modules
73# # Then manually: oe-go-mod-fetcher.py --discovered-modules ... --git-repo ...
74#
75# FULL AUTOMATIC EXAMPLE (all-in-one discovery + generation):
76#
77# TAGS = "netcgo osusergo"
78# GO_MOD_DISCOVERY_BUILD_TARGET = "./cmd/myapp"
79# GO_MOD_DISCOVERY_GIT_REPO = "https://github.com/example/myapp.git"
80# inherit go-mod-discovery
81# # Run: bitbake myapp -c discover_modules
82# # Recipe files are automatically regenerated!
83#
84# See: meta-virtualization/scripts/BOOTSTRAP-STRATEGY.md (Approach B)
85#
86# This task is NOT part of the normal build - it must be explicitly invoked
87# via bitbake <recipe> -c discover_modules
88#
89# PERSISTENT CACHE: The discovery cache is stored in ${TOPDIR}/go-mod-discovery/${PN}/${PV}/
90# instead of ${WORKDIR}. This ensures the cache survives `bitbake <recipe> -c cleanall`
91# since TOPDIR is the build directory root (e.g., /path/to/build/).
92# To clean the discovery cache, run: rm -rf ${TOPDIR}/go-mod-discovery/${PN}/${PV}/
93
94# Required variable (must be set by recipe)
95GO_MOD_DISCOVERY_BUILD_TARGET ?= ""
96
97# Optional variables with sensible defaults for standard Go recipe layout
98GO_MOD_DISCOVERY_SRCDIR ?= "${S}/src/import"
99GO_MOD_DISCOVERY_BUILD_TAGS ?= "${TAGS}"
100GO_MOD_DISCOVERY_LDFLAGS ?= "-w -s"
101GO_MOD_DISCOVERY_GOPATH ?= "${S}/src/import/.gopath:${S}/src/import/vendor"
102GO_MOD_DISCOVERY_OUTPUT ?= "${WORKDIR}/discovery-build-output"
103
104# Persistent discovery cache location - survives cleanall
105GO_MOD_DISCOVERY_DIR ?= "${TOPDIR}/go-mod-discovery/${PN}/${PV}"
106
107# Output JSON file for discovered modules (used by oe-go-mod-fetcher.py --discovered-modules)
108GO_MOD_DISCOVERY_MODULES_JSON ?= "${GO_MOD_DISCOVERY_DIR}/modules.json"
109
110# Set to "1" to skip automatic extraction (only download modules, don't extract metadata)
111GO_MOD_DISCOVERY_SKIP_EXTRACT ?= "0"
112
113# Set to "1" to skip automatic recipe regeneration (only discover and extract)
114GO_MOD_DISCOVERY_SKIP_GENERATE ?= "0"
115
116# Git repository URL for recipe generation (required if SKIP_GENERATE != "1")
117# Example: "https://github.com/rancher/k3s.git"
118GO_MOD_DISCOVERY_GIT_REPO ?= ""
119
120# Git ref (commit/tag) for recipe generation - defaults to recipe's SRCREV
121GO_MOD_DISCOVERY_GIT_REF ?= "${SRCREV}"
122
123# Recipe directory for generated .inc files - defaults to recipe's directory
124GO_MOD_DISCOVERY_RECIPEDIR ?= "${FILE_DIRNAME}"
125
126# Empty default for TAGS if not set by recipe (avoids undefined variable errors)
127TAGS ?= ""
128
129# Shell task that mirrors do_compile but with network access and discovery GOMODCACHE
130do_discover_modules() {
131 # Validate required variable
132 if [ -z "${GO_MOD_DISCOVERY_BUILD_TARGET}" ]; then
133 bbfatal "GO_MOD_DISCOVERY_BUILD_TARGET must be set (e.g., './cmd/server' or './...')"
134 fi
135
136 # Validate source directory exists and contains go.mod
137 if [ ! -d "${GO_MOD_DISCOVERY_SRCDIR}" ]; then
138 bbfatal "GO_MOD_DISCOVERY_SRCDIR does not exist: ${GO_MOD_DISCOVERY_SRCDIR}
139Hint: Set GO_MOD_DISCOVERY_SRCDIR in your recipe if go.mod is not in \${S}/src/import"
140 fi
141 if [ ! -f "${GO_MOD_DISCOVERY_SRCDIR}/go.mod" ]; then
142 bbfatal "go.mod not found in GO_MOD_DISCOVERY_SRCDIR: ${GO_MOD_DISCOVERY_SRCDIR}
143Hint: Set GO_MOD_DISCOVERY_SRCDIR to the directory containing go.mod"
144 fi
145
146 # Use PERSISTENT cache location outside WORKDIR to survive cleanall
147 # This is stored in ${TOPDIR}/go-mod-discovery/${PN}/${PV}/ so it persists
148 DISCOVERY_CACHE="${GO_MOD_DISCOVERY_DIR}/cache"
149
150 # Create required directories first
151 mkdir -p "${DISCOVERY_CACHE}"
152 mkdir -p "${WORKDIR}/go-tmp"
153 mkdir -p "$(dirname "${GO_MOD_DISCOVERY_OUTPUT}")"
154
155 # Use discovery-cache instead of the normal GOMODCACHE
156 export GOMODCACHE="${DISCOVERY_CACHE}"
157
158 # Enable network access to proxy.golang.org
159 export GOPROXY="https://proxy.golang.org,direct"
160 export GOSUMDB="sum.golang.org"
161
162 # Standard Go environment - use recipe-provided GOPATH or default
163 export GOPATH="${GO_MOD_DISCOVERY_GOPATH}:${STAGING_DIR_TARGET}/${prefix}/local/go"
164 export CGO_ENABLED="1"
165 export GOTOOLCHAIN="local"
166
167 # Use system temp directory for Go's work files
168 export GOTMPDIR="${WORKDIR}/go-tmp"
169
170 # Disable excessive debug output from BitBake environment
171 unset GODEBUG
172
173 # Build tags from recipe configuration
174 TAGS="${GO_MOD_DISCOVERY_BUILD_TAGS}"
175
176 # Change to source directory
177 cd "${GO_MOD_DISCOVERY_SRCDIR}"
178
179 echo "======================================================================"
180 echo "MODULE DISCOVERY: ${PN} ${PV}"
181 echo "======================================================================"
182 echo "GOMODCACHE: ${GOMODCACHE}"
183 echo "GOPROXY: ${GOPROXY}"
184 echo "Source dir: ${GO_MOD_DISCOVERY_SRCDIR}"
185 echo "Build target: ${GO_MOD_DISCOVERY_BUILD_TARGET}"
186 echo "Build tags: ${TAGS:-<none>}"
187 echo "LDFLAGS: ${GO_MOD_DISCOVERY_LDFLAGS}"
188 echo ""
189
190 # Use native go binary (not cross-compiler)
191 GO_NATIVE="${STAGING_DIR_NATIVE}${bindir_native}/go"
192
193 # NOTE: Do NOT run go mod tidy during discovery - it can upgrade versions in go.mod
194 # without adding checksums to go.sum, causing version mismatches.
195 # The source's go.mod/go.sum should already be correct for the commit.
196 # echo "Running: go mod tidy"
197 # ${GO_NATIVE} mod tidy
198 # ${GO_NATIVE} mod download # If tidy is re-enabled, this ensures go.sum gets all checksums
199
200 echo ""
201 echo "Running: go build (to discover all modules)..."
202
203 # Build to discover ALL modules that would be used at compile time
204 # This is better than 'go mod download' because it handles build tags correctly
205 BUILD_CMD="${GO_NATIVE} build -v -trimpath"
206 if [ -n "${TAGS}" ]; then
207 BUILD_CMD="${BUILD_CMD} -tags \"${TAGS}\""
208 fi
209 BUILD_CMD="${BUILD_CMD} -ldflags \"${GO_MOD_DISCOVERY_LDFLAGS}\""
210 BUILD_CMD="${BUILD_CMD} -o \"${GO_MOD_DISCOVERY_OUTPUT}\" ${GO_MOD_DISCOVERY_BUILD_TARGET}"
211
212 echo "Executing: ${BUILD_CMD}"
213 eval ${BUILD_CMD}
214
215 echo ""
216 echo "Fetching ALL modules referenced in go.sum..."
217 # go build downloads .zip files but not always .info files
218 # We need .info files for VCS metadata (Origin.URL, Origin.Hash)
219 # Extract unique module@version pairs from go.sum and download each
220 # go.sum format: "module version/go.mod hash" or "module version hash"
221 #
222 # IMPORTANT: We must download ALL versions, including /go.mod-only entries!
223 # When GOPROXY=off during compile, Go may need these for dependency resolution.
224 # Strip the /go.mod suffix to get the base version, then download it.
225 awk '{gsub(/\/go\.mod$/, "", $2); print $1 "@" $2}' go.sum | sort -u | while read modver; do
226 ${GO_NATIVE} mod download "$modver" 2>/dev/null || true
227 done
228
229 # Download ALL modules in the complete dependency graph.
230 # The go.sum loop above only gets direct dependencies. Replace directives
231 # can introduce transitive deps that aren't in go.sum but are needed at
232 # compile time when GOPROXY=off. `go mod download all` resolves and
233 # downloads the entire module graph, including transitive dependencies.
234 echo ""
235 echo "Downloading complete module graph (including transitive deps)..."
236 ${GO_NATIVE} mod download all 2>&1 || echo "Warning: some modules may have failed to download"
237
238 # Additionally scan for any modules that go build downloaded but don't have .info
239 # This ensures we capture everything that was fetched dynamically
240 echo ""
241 echo "Ensuring .info files for all cached modules..."
242 find "${GOMODCACHE}/cache/download" -name "*.zip" 2>/dev/null | while read zipfile; do
243 # Extract module@version from path like: .../module/@v/version.zip
244 version=$(basename "$zipfile" .zip)
245 moddir=$(dirname "$zipfile")
246 infofile="${moddir}/${version}.info"
247 if [ ! -f "$infofile" ]; then
248 # Reconstruct module path from directory structure
249 # cache/download/github.com/foo/bar/@v/v1.0.0.zip -> github.com/foo/bar@v1.0.0
250 modpath=$(echo "$moddir" | sed "s|${GOMODCACHE}/cache/download/||" | sed 's|/@v$||')
251 echo " Fetching .info for: ${modpath}@${version}"
252 ${GO_NATIVE} mod download "${modpath}@${version}" 2>/dev/null || true
253 fi
254 done
255
256 # Download transitive deps of REPLACED modules.
257 # Replace directives can point to older versions whose deps aren't in the MVS
258 # graph. At compile time with GOPROXY=off, Go validates the replaced version's
259 # go.mod. We parse replace directives and download each replacement version,
260 # which fetches all its transitive dependencies.
261 echo ""
262 echo "Downloading dependencies of replaced modules..."
263
264 # Extract replace directives: "old_module => new_module new_version"
265 awk '/^replace \($/,/^\)$/ {if ($0 !~ /^replace|^\)/) print}' go.mod | \
266 grep "=>" | while read line; do
267 # Parse: github.com/foo/bar => github.com/baz/qux v1.2.3
268 new_module=$(echo "$line" | awk '{print $(NF-1)}')
269 new_version=$(echo "$line" | awk '{print $NF}')
270
271 if [ -n "$new_module" ] && [ -n "$new_version" ] && [ "$new_version" != "=>" ]; then
272 echo " Replace target: ${new_module}@${new_version}"
273 # Download this specific version - Go will fetch all its dependencies
274 ${GO_NATIVE} mod download "${new_module}@${new_version}" 2>/dev/null || true
275 fi
276 done
277
278 # Count modules discovered
279 MODULE_COUNT=$(find "${GOMODCACHE}/cache/download" -name "*.info" 2>/dev/null | wc -l)
280
281 echo ""
282 echo "======================================================================"
283 echo "DISCOVERY COMPLETE"
284 echo "======================================================================"
285 echo "Modules discovered: ${MODULE_COUNT}"
286 echo "Cache location: ${GOMODCACHE}"
287
288 # Extract module metadata automatically (unless skipped)
289 if [ "${GO_MOD_DISCOVERY_SKIP_EXTRACT}" != "1" ]; then
290 echo ""
291 echo "Extracting module metadata..."
292
293 # Find the extraction script relative to this class file
294 EXTRACT_SCRIPT="${COREBASE}/../meta-virtualization/scripts/extract-discovered-modules.py"
295 if [ ! -f "${EXTRACT_SCRIPT}" ]; then
296 # Try alternate location
297 EXTRACT_SCRIPT="$(dirname "${COREBASE}")/meta-virtualization/scripts/extract-discovered-modules.py"
298 fi
299 if [ ! -f "${EXTRACT_SCRIPT}" ]; then
300 # Last resort - search in layer path
301 for layer in ${BBLAYERS}; do
302 if [ -f "${layer}/scripts/extract-discovered-modules.py" ]; then
303 EXTRACT_SCRIPT="${layer}/scripts/extract-discovered-modules.py"
304 break
305 fi
306 done
307 fi
308
309 if [ -f "${EXTRACT_SCRIPT}" ]; then
310 python3 "${EXTRACT_SCRIPT}" \
311 --gomodcache "${GOMODCACHE}" \
312 --output "${GO_MOD_DISCOVERY_MODULES_JSON}"
313 EXTRACT_RC=$?
314 if [ $EXTRACT_RC -eq 0 ]; then
315 echo ""
316 echo "✓ Module metadata extracted to: ${GO_MOD_DISCOVERY_MODULES_JSON}"
317 else
318 bbwarn "Module extraction failed (exit code $EXTRACT_RC)"
319 bbwarn "You can run manually: python3 ${EXTRACT_SCRIPT} --gomodcache ${GOMODCACHE} --output ${GO_MOD_DISCOVERY_MODULES_JSON}"
320 EXTRACT_RC=1 # Mark as failed for generation check
321 fi
322 else
323 bbwarn "Could not find extract-discovered-modules.py script"
324 bbwarn "Run manually: extract-discovered-modules.py --gomodcache ${GOMODCACHE} --output ${GO_MOD_DISCOVERY_MODULES_JSON}"
325 EXTRACT_RC=1 # Mark as failed for generation check
326 fi
327 else
328 echo ""
329 echo "Skipping automatic extraction (GO_MOD_DISCOVERY_SKIP_EXTRACT=1)"
330 EXTRACT_RC=1 # Skip generation too if extraction skipped
331 fi
332
333 # Step 3: Generate recipe .inc files (unless skipped or extraction failed)
334 if [ "${GO_MOD_DISCOVERY_SKIP_GENERATE}" != "1" ] && [ "${EXTRACT_RC:-0}" = "0" ]; then
335 # Validate required git repo
336 if [ -z "${GO_MOD_DISCOVERY_GIT_REPO}" ]; then
337 bbwarn "GO_MOD_DISCOVERY_GIT_REPO not set - skipping recipe generation"
338 bbwarn "Set GO_MOD_DISCOVERY_GIT_REPO in your recipe to enable automatic generation"
339 echo ""
340 echo "NEXT STEP: Regenerate recipe manually:"
341 echo ""
342 echo " ./meta-virtualization/scripts/oe-go-mod-fetcher.py \\"
343 echo " --discovered-modules ${GO_MOD_DISCOVERY_MODULES_JSON} \\"
344 echo " --git-repo <your-git-repo-url> \\"
345 echo " --git-ref ${GO_MOD_DISCOVERY_GIT_REF} \\"
346 echo " --recipedir ${GO_MOD_DISCOVERY_RECIPEDIR}"
347 else
348 echo ""
349 echo "Generating recipe .inc files..."
350
351 # Find the fetcher script (same search as extraction script)
352 FETCHER_SCRIPT="${COREBASE}/../meta-virtualization/scripts/oe-go-mod-fetcher.py"
353 if [ ! -f "${FETCHER_SCRIPT}" ]; then
354 FETCHER_SCRIPT="$(dirname "${COREBASE}")/meta-virtualization/scripts/oe-go-mod-fetcher.py"
355 fi
356 if [ ! -f "${FETCHER_SCRIPT}" ]; then
357 for layer in ${BBLAYERS}; do
358 if [ -f "${layer}/scripts/oe-go-mod-fetcher.py" ]; then
359 FETCHER_SCRIPT="${layer}/scripts/oe-go-mod-fetcher.py"
360 break
361 fi
362 done
363 fi
364
365 if [ -f "${FETCHER_SCRIPT}" ]; then
366 python3 "${FETCHER_SCRIPT}" \
367 --discovered-modules "${GO_MOD_DISCOVERY_MODULES_JSON}" \
368 --git-repo "${GO_MOD_DISCOVERY_GIT_REPO}" \
369 --git-ref "${GO_MOD_DISCOVERY_GIT_REF}" \
370 --recipedir "${GO_MOD_DISCOVERY_RECIPEDIR}"
371 GENERATE_RC=$?
372 if [ $GENERATE_RC -eq 0 ]; then
373 echo ""
374 echo "✓ Recipe files regenerated in: ${GO_MOD_DISCOVERY_RECIPEDIR}"
375 else
376 bbwarn "Recipe generation failed (exit code $GENERATE_RC)"
377 bbwarn "Check the output above for errors"
378 fi
379 else
380 bbwarn "Could not find oe-go-mod-fetcher.py script"
381 bbwarn "Run manually: oe-go-mod-fetcher.py --discovered-modules ${GO_MOD_DISCOVERY_MODULES_JSON} --git-repo ${GO_MOD_DISCOVERY_GIT_REPO} --git-ref ${GO_MOD_DISCOVERY_GIT_REF} --recipedir ${GO_MOD_DISCOVERY_RECIPEDIR}"
382 fi
383 fi
384 elif [ "${GO_MOD_DISCOVERY_SKIP_GENERATE}" = "1" ]; then
385 echo ""
386 echo "Skipping automatic generation (GO_MOD_DISCOVERY_SKIP_GENERATE=1)"
387 echo ""
388 echo "NEXT STEP: Regenerate recipe manually:"
389 echo ""
390 echo " ./meta-virtualization/scripts/oe-go-mod-fetcher.py \\"
391 echo " --discovered-modules ${GO_MOD_DISCOVERY_MODULES_JSON} \\"
392 echo " --git-repo <your-git-repo-url> \\"
393 echo " --git-ref <your-git-ref> \\"
394 echo " --recipedir ${GO_MOD_DISCOVERY_RECIPEDIR}"
395 fi
396
397 echo ""
398 echo "NOTE: Cache is stored OUTSIDE WORKDIR in a persistent location."
399 echo " This cache survives 'bitbake ${PN} -c cleanall'!"
400 echo " To clean: rm -rf ${GO_MOD_DISCOVERY_DIR}"
401 echo ""
402 echo "======================================================================"
403}
404
405# Make this task manually runnable (not part of default build)
406# Run after unpack and patch so source is available
407addtask discover_modules after do_patch
408
409# Task dependencies - need source unpacked and full toolchain available
410# Depend on do_prepare_recipe_sysroot to get cross-compiler for CGO
411do_discover_modules[depends] = "${PN}:do_prepare_recipe_sysroot"
412
413# Enable network access for this task ONLY
414do_discover_modules[network] = "1"
415
416# Don't create stamp file - allow running multiple times
417do_discover_modules[nostamp] = "1"
418
419# Track all configuration variables for proper task hashing
420do_discover_modules[vardeps] += "GO_MOD_DISCOVERY_DIR GO_MOD_DISCOVERY_SRCDIR \
421 GO_MOD_DISCOVERY_BUILD_TARGET GO_MOD_DISCOVERY_BUILD_TAGS \
422 GO_MOD_DISCOVERY_LDFLAGS GO_MOD_DISCOVERY_GOPATH GO_MOD_DISCOVERY_OUTPUT \
423 GO_MOD_DISCOVERY_MODULES_JSON GO_MOD_DISCOVERY_SKIP_EXTRACT \
424 GO_MOD_DISCOVERY_SKIP_GENERATE GO_MOD_DISCOVERY_GIT_REPO \
425 GO_MOD_DISCOVERY_GIT_REF GO_MOD_DISCOVERY_RECIPEDIR"
426
427# Task to clean the persistent discovery cache
428# Usage: bitbake <recipe> -c clean_discovery
429do_clean_discovery() {
430 if [ -d "${GO_MOD_DISCOVERY_DIR}" ]; then
431 echo "Removing discovery cache: ${GO_MOD_DISCOVERY_DIR}"
432 rm -rf "${GO_MOD_DISCOVERY_DIR}"
433 echo "Discovery cache removed."
434 else
435 echo "Discovery cache not found: ${GO_MOD_DISCOVERY_DIR}"
436 fi
437}
438
439addtask clean_discovery
440do_clean_discovery[nostamp] = "1"
441do_clean_discovery[vardeps] += "GO_MOD_DISCOVERY_DIR"