alef 0.25.37

Opinionated polyglot binding generator for Rust libraries
Documentation
// Tool to download platform-specific FFI libraries from GitHub releases.
// Invoked via `go generate` before compilation.
//go:build ignore
// +build ignore

package main

import (
	"archive/tar"
	"compress/gzip"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strings"
)

const (
	moduleVersion = "{{ crate_version }}"
	repoURL       = "{{ repo_url }}"
	assetPrefix   = "{{ asset_prefix }}"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
}

func run() error {
	// Determine download cache and library paths
	cacheBase, libPath, bindingLibDir, err := determinePaths()
	if err != nil {
		return err
	}

	// Check if library already exists
	if _, err := os.Stat(libPath); err == nil {
		// Library already cached, nothing to do
		return nil
	}

	// Download the FFI library tarball
	if err := downloadAndExtractLibrary(cacheBase); err != nil {
		return fmt.Errorf("failed to download FFI library: %w", err)
	}

	// Copy library from cache to binding package directory so that cgo LDFLAGS
	// can find it via ${SRCDIR}/.lib/ whether in module cache or vendored.
	if err := copyLibraryToBindingPackage(cacheBase, bindingLibDir); err != nil {
		return fmt.Errorf("failed to copy library to binding package: %w", err)
	}

	return nil
}

func determinePaths() (string, string, string, error) {
	goos := runtime.GOOS
	goarch := runtime.GOARCH

	// Map Go platform names to asset names
	osName := goos
	if goos == "darwin" {
		osName = "macos"
	}

	libName := "{{ ffi_lib_name }}"
	// go generate changes to the module directory before running directives.
	// cwd is the module's root (binding.go is at this level).
	moduleRoot, err := os.Getwd()
	if err != nil {
		return "", "", "", fmt.Errorf("cannot determine module root: %w", err)
	}

	platformDir := fmt.Sprintf("%s-%s", osName, goarch)
	libDir := filepath.Join(moduleRoot, ".lib", platformDir)
	libPath := filepath.Join(libDir, libFilename(libName, goos))

	// Binding package directory is the same as module root (where binding.go lives).
	bindingLibDir := filepath.Join(moduleRoot, ".lib", platformDir)

	return libDir, libPath, bindingLibDir, nil
}

func libFilename(libName, goos string) string {
	switch goos {
	case "windows":
		return libName + ".dll"
	case "darwin":
		return "lib" + libName + ".dylib"
	default:
		return "lib" + libName + ".so"
	}
}

func downloadAndExtractLibrary(cacheDir string) error {
	goos := runtime.GOOS
	goarch := runtime.GOARCH

	osName := goos
	if goos == "darwin" {
		osName = "macos"
	}

	// Map Go arch names to the alef platform names used in release asset filenames.
	// The local .lib/<os>-<goarch>/ directories use Go arch names (matching cgo LDFLAGS),
	// but alef's packager emits tarballs with its own arch names: x86_64, aarch64.
	archName := goarch
	switch goarch {
	case "amd64":
		archName = "x86_64"
	case "arm64":
		// macOS arm64 stays "arm64" (alef go_java_platform special-cases it);
		// all other platforms use "aarch64".
		if goos != "darwin" {
			archName = "aarch64"
		}
	}

	// Clean version for the download URL path; asset name itself is unversioned.
	version := strings.TrimPrefix(moduleVersion, "v")
	assetName := fmt.Sprintf("%s-go-%s-%s.tar.gz", assetPrefix, osName, archName)
	downloadURL := fmt.Sprintf("%s/releases/download/v%s/%s", repoURL, version, assetName)

	// Create cache directory
	if err := os.MkdirAll(cacheDir, 0755); err != nil {
		return fmt.Errorf("mkdir cache: %w", err)
	}

	// Download tarball
	resp, err := http.Get(downloadURL)
	if err != nil {
		return fmt.Errorf("download %s: %w", downloadURL, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
	}

	// Extract tarball to cache directory
	if err := extractTarGz(resp.Body, cacheDir); err != nil {
		return fmt.Errorf("extract tarball: %w", err)
	}

	return nil
}

func extractTarGz(src io.Reader, dstDir string) error {
	gzr, err := gzip.NewReader(src)
	if err != nil {
		return err
	}
	defer gzr.Close()

	tr := tar.NewReader(gzr)
	for {
		header, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}

		// Construct destination path, stripping any leading directory
		// from the tarball (e.g., "staging/lib..." -> "lib...")
		targetPath := filepath.Join(dstDir, filepath.Base(header.Name))

		switch header.Typeflag {
		case tar.TypeReg:
			f, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
			if err != nil {
				return err
			}
			if _, err := io.Copy(f, tr); err != nil {
				f.Close()
				return err
			}
			f.Close()

		case tar.TypeDir:
			if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
				return err
			}
		}
	}

	return nil
}

func copyLibraryToBindingPackage(srcDir, dstDir string) error {
	// Create destination directory
	if err := os.MkdirAll(dstDir, 0755); err != nil {
		return fmt.Errorf("mkdir %s: %w", dstDir, err)
	}

	// determinePaths returns the same path for cache and binding library dirs,
	// so source and destination point at the same files. os.Create() truncates
	// the destination first, which (when paths alias) zeroes the source mid-copy
	// — leaving the dylib at 0 bytes and breaking cgo linking. Skip the copy
	// when there's nothing to move.
	absSrc, srcErr := filepath.Abs(srcDir)
	absDst, dstErr := filepath.Abs(dstDir)
	if srcErr == nil && dstErr == nil && absSrc == absDst {
		return nil
	}

	// List files in source directory
	entries, err := os.ReadDir(srcDir)
	if err != nil {
		return fmt.Errorf("read directory %s: %w", srcDir, err)
	}

	// Copy each library file
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		srcPath := filepath.Join(srcDir, entry.Name())
		dstPath := filepath.Join(dstDir, entry.Name())

		// Copy file
		src, err := os.Open(srcPath)
		if err != nil {
			return fmt.Errorf("open %s: %w", srcPath, err)
		}
		defer src.Close()

		dst, err := os.Create(dstPath)
		if err != nil {
			return fmt.Errorf("create %s: %w", dstPath, err)
		}
		defer dst.Close()

		if _, err := io.Copy(dst, src); err != nil {
			return fmt.Errorf("copy %s -> %s: %w", srcPath, dstPath, err)
		}
	}

	return nil
}