From 38082f6e8cab87ba83220efe432135c9f4590218 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 27 Mar 2026 12:23:56 -0700 Subject: [PATCH 1/7] Use the ownership model introduced in 76d6468 --- ownership_regression_test.go | 225 +++++++++++++++++++++++++++++++++++ value_helper.go | 14 --- 2 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 ownership_regression_test.go diff --git a/ownership_regression_test.go b/ownership_regression_test.go new file mode 100644 index 0000000..0abbb7d --- /dev/null +++ b/ownership_regression_test.go @@ -0,0 +1,225 @@ +package lbug + +import ( + "os" + "os/exec" + "runtime" + "runtime/debug" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBorrowedValueOwnershipRecursiveRelationship(t *testing.T) { + runBorrowedValueOwnershipScenario(t, "recursive_relationship") +} + +func TestBorrowedValueOwnershipNestedContainers(t *testing.T) { + runBorrowedValueOwnershipScenario(t, "nested_containers") +} + +func TestBorrowedValueOwnershipSubprocess(t *testing.T) { + scenario := os.Getenv("LBUG_BORROWED_VALUE_SCENARIO") + if scenario == "" { + t.Skip("subprocess helper") + } + + prevGCPercent := debug.SetGCPercent(1) + defer debug.SetGCPercent(prevGCPercent) + + switch scenario { + case "recursive_relationship": + runRecursiveRelationshipOwnershipStress(t) + case "nested_containers": + runNestedContainerOwnershipStress(t) + default: + t.Fatalf("unknown ownership scenario %q", scenario) + } +} + +func runBorrowedValueOwnershipScenario(t *testing.T, scenario string) { + t.Helper() + + cmd := exec.Command(os.Args[0], "-test.run=^TestBorrowedValueOwnershipSubprocess$") + cmd.Env = append(os.Environ(), "LBUG_BORROWED_VALUE_SCENARIO="+scenario) + output, err := cmd.CombinedOutput() + require.NoError(t, err, "scenario %s crashed:\n%s", scenario, string(output)) +} + +func runRecursiveRelationshipOwnershipStress(t *testing.T) { + db, conn := setupTestDatabase(t) + defer db.Close() + defer conn.Close() + + createTestData(t, conn, 250) + + runOwnershipStress(t, 8, 12, func() error { + return queryRecursiveRelationships(conn) + }) +} + +func runNestedContainerOwnershipStress(t *testing.T) { + _, conn := SetupTestDatabase(t) + + runOwnershipStress(t, 8, 20, func() error { + return queryNestedContainers(conn) + }) +} + +func runOwnershipStress(t *testing.T, numGoroutines int, queriesPerGoroutine int, queryFn func() error) { + t.Helper() + + var wg sync.WaitGroup + errChan := make(chan error, numGoroutines*queriesPerGoroutine) + + for range numGoroutines { + wg.Add(1) + go func() { + defer wg.Done() + + for range queriesPerGoroutine { + if err := queryFn(); err != nil { + errChan <- err + return + } + runtime.GC() + } + }() + } + + wg.Wait() + close(errChan) + + for err := range errChan { + require.NoError(t, err) + } +} + +func queryRecursiveRelationships(conn *Connection) error { + result, err := conn.Query(` + MATCH (source:Node {id: 0})-[r:CONNECTS* ALL SHORTEST 1..3]->(target:Node {id: 4}) + RETURN r, source.fqn, target.fqn + `) + if err != nil { + return err + } + defer result.Close() + + rowCount := 0 + for result.HasNext() { + row, err := result.Next() + if err != nil { + return err + } + + value, err := row.GetValue(0) + if err != nil { + row.Close() + return err + } + + recursiveRel := value.(RecursiveRelationship) + if len(recursiveRel.Relationships) != 2 { + row.Close() + return requireLengthError("recursive relationships", 2, len(recursiveRel.Relationships)) + } + + row.Close() + rowCount++ + } + + if rowCount == 0 { + return requireLengthError("recursive relationship rows", 1, 0) + } + + return nil +} + +func queryNestedContainers(conn *Connection) error { + result, err := conn.Query(` + MATCH (p:person)-[r:workAt]->(o:organisation) + WHERE p.ID = 5 + RETURN + p, + r, + p.courseScoresPerTerm, + { + name: p.fName, + scores: p.courseScoresPerTerm, + usedNames: p.usedNames + }, + o + `) + if err != nil { + return err + } + defer result.Close() + + if !result.HasNext() { + return requireLengthError("nested container rows", 1, 0) + } + + row, err := result.Next() + if err != nil { + return err + } + defer row.Close() + + values, err := row.GetAsSlice() + if err != nil { + return err + } + if len(values) != 5 { + return requireLengthError("nested container values", 5, len(values)) + } + + person := values[0].(Node) + if person.Label != "person" || person.Properties["ID"] != int64(5) { + return errUnexpectedValue("person payload") + } + + rel := values[1].(Relationship) + if rel.Label != "workAt" || rel.Properties["year"] != int64(2010) { + return errUnexpectedValue("relationship payload") + } + + scores := values[2].([]any) + if len(scores) == 0 { + return errUnexpectedValue("scores payload") + } + + profile := values[3].(map[string]any) + if profile["name"] != person.Properties["fName"] { + return errUnexpectedValue("profile payload") + } + + org := values[4].(Node) + if org.Label != "organisation" { + return errUnexpectedValue("organisation payload") + } + + return nil +} + +func requireLengthError(name string, want int, got int) error { + return &ownershipError{name: name, message: "unexpected length", want: want, got: got} +} + +func errUnexpectedValue(name string) error { + return &ownershipError{name: name, message: "unexpected value"} +} + +type ownershipError struct { + name string + message string + want int + got int +} + +func (e *ownershipError) Error() string { + if e.want == 0 && e.got == 0 { + return e.name + ": " + e.message + } + return e.name + ": " + e.message +} diff --git a/value_helper.go b/value_helper.go index 6d146b7..4247de0 100644 --- a/value_helper.go +++ b/value_helper.go @@ -66,12 +66,10 @@ func lbugNodeValueToGoValue(lbugValue C.lbug_value) (Node, error) { C.lbug_node_val_get_id_val(&lbugValue, &idValue) nodeId, _ := lbugValueToGoValue(idValue) node.ID = nodeId.(InternalID) - C.lbug_value_destroy(&idValue) labelValue := C.lbug_value{} C.lbug_node_val_get_label_val(&lbugValue, &labelValue) nodeLabel, _ := lbugValueToGoValue(labelValue) node.Label = nodeLabel.(string) - C.lbug_value_destroy(&labelValue) var propertySize C.uint64_t C.lbug_node_val_get_property_size(&lbugValue, &propertySize) var currentKey *C.char @@ -87,7 +85,6 @@ func lbugNodeValueToGoValue(lbugValue C.lbug_value) (Node, error) { errors = append(errors, err) } node.Properties[keyString] = value - C.lbug_value_destroy(¤tVal) } if len(errors) > 0 { return node, fmt.Errorf("failed to get values: %v", errors) @@ -104,20 +101,16 @@ func lbugRelValueToGoValue(lbugValue C.lbug_value) (Relationship, error) { C.lbug_rel_val_get_id_val(&lbugValue, &idValue) id, _ := lbugValueToGoValue(idValue) relation.ID = id.(InternalID) - C.lbug_value_destroy(&idValue) C.lbug_rel_val_get_src_id_val(&lbugValue, &idValue) src, _ := lbugValueToGoValue(idValue) relation.SourceID = src.(InternalID) - C.lbug_value_destroy(&idValue) C.lbug_rel_val_get_dst_id_val(&lbugValue, &idValue) dst, _ := lbugValueToGoValue(idValue) relation.DestinationID = dst.(InternalID) - C.lbug_value_destroy(&idValue) labelValue := C.lbug_value{} C.lbug_rel_val_get_label_val(&lbugValue, &labelValue) label, _ := lbugValueToGoValue(labelValue) relation.Label = label.(string) - C.lbug_value_destroy(&labelValue) var propertySize C.uint64_t C.lbug_rel_val_get_property_size(&lbugValue, &propertySize) var currentKey *C.char @@ -133,7 +126,6 @@ func lbugRelValueToGoValue(lbugValue C.lbug_value) (Relationship, error) { errors = append(errors, err) } relation.Properties[keyString] = value - C.lbug_value_destroy(¤tVal) } if len(errors) > 0 { return relation, fmt.Errorf("failed to get values: %v", errors) @@ -148,8 +140,6 @@ func lbugRecursiveRelValueToGoValue(lbugValue C.lbug_value) (RecursiveRelationsh var relsVal C.lbug_value C.lbug_value_get_recursive_rel_node_list(&lbugValue, &nodesVal) C.lbug_value_get_recursive_rel_rel_list(&lbugValue, &relsVal) - defer C.lbug_value_destroy(&nodesVal) - defer C.lbug_value_destroy(&relsVal) nodes, _ := lbugListValueToGoValue(nodesVal) rels, _ := lbugListValueToGoValue(relsVal) recursiveRel := RecursiveRelationship{} @@ -188,7 +178,6 @@ func lbugListValueToGoValue(lbugValue C.lbug_value) ([]any, error) { errors = append(errors, err) } list = append(list, value) - C.lbug_value_destroy(¤tVal) } if len(errors) > 0 { return list, fmt.Errorf("failed to get values: %v", errors) @@ -215,7 +204,6 @@ func lbugStructValueToGoValue(lbugValue C.lbug_value) (map[string]any, error) { errors = append(errors, err) } structure[keyString] = value - C.lbug_value_destroy(¤tVal) } if len(errors) > 0 { return structure, fmt.Errorf("failed to get values: %v", errors) @@ -243,8 +231,6 @@ func lbugMapValueToGoValue(lbugValue C.lbug_value) ([]MapItem, error) { if err != nil { errors = append(errors, err) } - C.lbug_value_destroy(¤tKey) - C.lbug_value_destroy(¤tValue) mapItems = append(mapItems, MapItem{Key: key, Value: value}) } if len(errors) > 0 { From 78e8dc69fab5dcba440d7a3df828bc349b2e76a6 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 29 May 2026 10:31:59 -0700 Subject: [PATCH 2/7] Validate collection parameter element types --- value_helper.go | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/value_helper.go b/value_helper.go index 4247de0..93a9177 100644 --- a/value_helper.go +++ b/value_helper.go @@ -465,6 +465,18 @@ func int128ToBigInt(value C.lbug_int128_t) (*big.Int, error) { return bigInt, nil } +func lbugValuesHaveSameDataType(left *C.lbug_value, right *C.lbug_value) bool { + var leftType C.lbug_logical_type + C.lbug_value_get_data_type(left, &leftType) + defer C.lbug_data_type_destroy(&leftType) + + var rightType C.lbug_logical_type + C.lbug_value_get_data_type(right, &rightType) + defer C.lbug_data_type_destroy(&rightType) + + return bool(C.lbug_data_type_equals(&leftType, &rightType)) +} + // goMapToLbugStruct converts a map of string to any to a lbug_value representing // a STRUCT. It returns an error if the map is empty. func goMapToLbugStruct(value map[string]any) (*C.lbug_value, error) { @@ -483,14 +495,15 @@ func goMapToLbugStruct(value map[string]any) (*C.lbug_value, error) { } sort.Strings(sortedKeys) for _, k := range sortedKeys { - fieldNames = append(fieldNames, C.CString(k)) + fieldName := C.CString(k) + fieldNames = append(fieldNames, fieldName) + defer C.free(unsafe.Pointer(fieldName)) lbugValue, error := goValueToLbugValue(value[k]) if error != nil { return nil, fmt.Errorf("failed to convert value in the map with error: %w", error) } fieldValues = append(fieldValues, lbugValue) defer C.lbug_value_destroy(lbugValue) - defer C.free(unsafe.Pointer(C.CString(k))) } var lbugValue *C.lbug_value @@ -512,6 +525,8 @@ func goSliceOfMapItemsToLbugMap(slice []MapItem) (*C.lbug_value, error) { } keys := make([]*C.lbug_value, 0, len(slice)) values := make([]*C.lbug_value, 0, len(slice)) + var firstKey *C.lbug_value + var firstValue *C.lbug_value for _, item := range slice { key, error := goValueToLbugValue(item.Key) if error != nil { @@ -519,12 +534,26 @@ func goSliceOfMapItemsToLbugMap(slice []MapItem) (*C.lbug_value, error) { } keys = append(keys, key) defer C.lbug_value_destroy(key) + if !bool(C.lbug_value_is_null(key)) { + if firstKey == nil { + firstKey = key + } else if !lbugValuesHaveSameDataType(firstKey, key) { + return nil, fmt.Errorf("failed to create MAP value with status: %d. please make sure all the keys are of the same type and all the values are of the same type", C.LbugError) + } + } value, error := goValueToLbugValue(item.Value) if error != nil { return nil, fmt.Errorf("failed to convert value in the slice with error: %w", error) } values = append(values, value) defer C.lbug_value_destroy(value) + if !bool(C.lbug_value_is_null(value)) { + if firstValue == nil { + firstValue = value + } else if !lbugValuesHaveSameDataType(firstValue, value) { + return nil, fmt.Errorf("failed to create MAP value with status: %d. please make sure all the keys are of the same type and all the values are of the same type", C.LbugError) + } + } } var lbugValue *C.lbug_value status := C.lbug_value_create_map(numItems, &keys[0], &values[0], &lbugValue) @@ -543,6 +572,7 @@ func goSliceToLbugList(slice []any) (*C.lbug_value, error) { return nil, fmt.Errorf("failed to create LIST value because the slice is empty") } values := make([]*C.lbug_value, 0, len(slice)) + var firstValue *C.lbug_value for _, item := range slice { value, error := goValueToLbugValue(item) if error != nil { @@ -550,6 +580,13 @@ func goSliceToLbugList(slice []any) (*C.lbug_value, error) { } values = append(values, value) defer C.lbug_value_destroy(value) + if !bool(C.lbug_value_is_null(value)) { + if firstValue == nil { + firstValue = value + } else if !lbugValuesHaveSameDataType(firstValue, value) { + return nil, fmt.Errorf("failed to create LIST value with status: %d. please make sure all the values are of the same type", C.LbugError) + } + } } var lbugValue *C.lbug_value status := C.lbug_value_create_list(numItems, &values[0], &lbugValue) From a8c1af3bc90e739fcfa1ea40564cf6023c1d1e24 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 29 May 2026 10:32:41 -0700 Subject: [PATCH 3/7] Add missing download script --- download-liblbug.sh | 139 ++++++++++++++++++++++++++++++++++++++++++++ download_lbug.sh | 90 ++++++++++++++++------------ 2 files changed, 193 insertions(+), 36 deletions(-) create mode 100755 download-liblbug.sh diff --git a/download-liblbug.sh b/download-liblbug.sh new file mode 100755 index 0000000..132904c --- /dev/null +++ b/download-liblbug.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Download prebuilt liblbug archives from GitHub releases or workflow artifacts. +set -euo pipefail + +LIB_KIND="${LBUG_LIB_KIND:-shared}" +LINUX_VARIANT="${LBUG_LINUX_VARIANT:-compat}" +REPOSITORY="${LBUG_GITHUB_REPOSITORY:-LadybugDB/ladybug}" +RUN_ID="${LBUG_PRECOMPILED_RUN_ID:-}" +VERSION_OVERRIDE="${LBUG_VERSION:-}" + +if [ "$LIB_KIND" != "shared" ] && [ "$LIB_KIND" != "static" ]; then + echo "Unsupported LBUG_LIB_KIND: $LIB_KIND (expected 'shared' or 'static')" >&2 + exit 1 +fi + +if [ "$LINUX_VARIANT" != "compat" ] && [ "$LINUX_VARIANT" != "perf" ]; then + echo "Unsupported LBUG_LINUX_VARIANT: $LINUX_VARIANT (expected 'compat' or 'perf')" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TARGET_DIR="${LBUG_TARGET_DIR:-$PROJECT_DIR/lib}" + +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Darwin) + if [ "$ARCH" = "x86_64" ]; then + MACOS_ARCHIVE_ARCH="x86_64" + elif [ "$ARCH" = "arm64" ]; then + MACOS_ARCHIVE_ARCH="arm64" + else + echo "Unsupported macOS architecture: $ARCH" >&2 + exit 1 + fi + if [ "$LIB_KIND" = "static" ]; then + ARCHIVE="liblbug-static-osx-${MACOS_ARCHIVE_ARCH}.tar.gz" + ARTIFACT_NAME="liblbug-static-osx-${MACOS_ARCHIVE_ARCH}" + LIB_NAME="liblbug.a" + else + ARCHIVE="liblbug-osx-${MACOS_ARCHIVE_ARCH}.tar.gz" + ARTIFACT_NAME="liblbug-osx-${MACOS_ARCHIVE_ARCH}" + LIB_NAME="liblbug.dylib" + fi + ;; + Linux) + if [ "$ARCH" = "x86_64" ]; then + LINUX_ARCHIVE_ARCH="x86_64" + elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then + LINUX_ARCHIVE_ARCH="aarch64" + else + echo "Unsupported Linux architecture: $ARCH" >&2 + exit 1 + fi + if [ "$LIB_KIND" = "static" ]; then + ARCHIVE="liblbug-static-linux-${LINUX_ARCHIVE_ARCH}-${LINUX_VARIANT}.tar.gz" + ARTIFACT_NAME="liblbug-static-linux-${LINUX_ARCHIVE_ARCH}-${LINUX_VARIANT}" + LIB_NAME="liblbug.a" + else + ARCHIVE="liblbug-linux-${LINUX_ARCHIVE_ARCH}.tar.gz" + ARTIFACT_NAME="liblbug-linux-${LINUX_ARCHIVE_ARCH}" + LIB_NAME="liblbug.so" + fi + ;; + MINGW*|MSYS*|CYGWIN*) + if [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "AMD64" ]; then + WINDOWS_ARCHIVE_ARCH="x86_64" + else + echo "Unsupported Windows architecture: $ARCH" >&2 + exit 1 + fi + if [ "$LIB_KIND" = "static" ]; then + ARCHIVE="liblbug-static-windows-${WINDOWS_ARCHIVE_ARCH}.zip" + ARTIFACT_NAME="liblbug-static-windows-${WINDOWS_ARCHIVE_ARCH}" + LIB_NAME="lbug.lib" + else + ARCHIVE="liblbug-windows-${WINDOWS_ARCHIVE_ARCH}.zip" + ARTIFACT_NAME="liblbug-windows-${WINDOWS_ARCHIVE_ARCH}" + LIB_NAME="lbug_shared.dll" + fi + ;; + *) + echo "Unsupported OS: $OS" >&2 + exit 1 + ;; +esac + +if [ -f "$TARGET_DIR/$LIB_NAME" ]; then + echo "liblbug already exists in $TARGET_DIR" + exit 0 +fi + +mkdir -p "$TARGET_DIR" +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +fetch_release_archive() { + local version + if [ -n "$VERSION_OVERRIDE" ]; then + version="$VERSION_OVERRIDE" + else + version="$(curl -sS "https://api.github.com/repos/${REPOSITORY}/releases/latest" | grep -o '"tag_name": "v\([^"]*\)"' | cut -d'"' -f4 | cut -c2-)" + fi + local download_url="https://github.com/${REPOSITORY}/releases/download/v${version}/${ARCHIVE}" + curl -fSL "$download_url" -o "$TMPDIR/$ARCHIVE" + echo "release:v${version}" +} + +fetch_run_artifact() { + if ! command -v gh >/dev/null 2>&1; then + echo "gh CLI is required when LBUG_PRECOMPILED_RUN_ID is set" >&2 + exit 1 + fi + gh run download "$RUN_ID" --repo "$REPOSITORY" --name "$ARTIFACT_NAME" --dir "$TMPDIR/artifact" >/dev/null + local extracted_archive + extracted_archive="$(find "$TMPDIR/artifact" -type f -name "$ARCHIVE" | head -n1)" + if [ -z "$extracted_archive" ]; then + echo "Artifact ${ARTIFACT_NAME} does not contain ${ARCHIVE}" >&2 + exit 1 + fi + mv "$extracted_archive" "$TMPDIR/$ARCHIVE" + echo "run:${RUN_ID}/${ARTIFACT_NAME}" +} + +if [ -n "$RUN_ID" ]; then + SOURCE_DESC="$(fetch_run_artifact)" +else + SOURCE_DESC="$(fetch_release_archive)" +fi + +if [[ "$ARCHIVE" == *.zip ]]; then + unzip -o "$TMPDIR/$ARCHIVE" -d "$TARGET_DIR" +else + tar xzf "$TMPDIR/$ARCHIVE" -C "$TARGET_DIR" +fi + +echo "Installed ${ARCHIVE} from ${SOURCE_DESC} to $TARGET_DIR" diff --git a/download_lbug.sh b/download_lbug.sh index bb3368b..85b3ee2 100755 --- a/download_lbug.sh +++ b/download_lbug.sh @@ -1,51 +1,69 @@ #!/bin/sh -# Wrapper around download-liblbug.sh that places the library into lib/ where -# cgo_bundled.go expects it, then creates the versioned symlink that the -# runtime dynamic linker needs (the dylib/so embed a versioned install name). -# -# download-liblbug.sh is kept as a verbatim copy of the upstream script at: -# https://raw.githubusercontent.com/LadybugDB/ladybug/refs/heads/main/scripts/download-liblbug.sh -# To update it: curl -fsSL -o download-liblbug.sh - -# pipefail is a bashism; use -eu which POSIX sh supports. -# The script contains no pipelines, so pipefail is not needed here. +# Wrapper around upstream download-liblbug.sh (same pattern as go-ladybug). +# Downloads prebuilt liblbug into a local cache and writes CMake env flags. set -eu SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -LIB_DIR="$SCRIPT_DIR/lib" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +ENV_FILE="${1:-$PROJECT_DIR/.cache/lbug-prebuilt.env}" +CACHE_LIB_DIR="${LBUG_TARGET_DIR:-$PROJECT_DIR/.cache/lbug-prebuilt/lib}" +LIB_KIND="${LBUG_LIB_KIND:-static}" UPSTREAM_SCRIPT="$SCRIPT_DIR/download-liblbug.sh" UPSTREAM_URL="https://raw.githubusercontent.com/LadybugDB/ladybug/refs/heads/main/scripts/download-liblbug.sh" -# Fetch the upstream helper script if it is not already present. +# Fetch the upstream helper if needed. if [ ! -f "$UPSTREAM_SCRIPT" ]; then echo "Fetching $UPSTREAM_URL ..." curl -fsSL "$UPSTREAM_URL" -o "$UPSTREAM_SCRIPT" chmod +x "$UPSTREAM_SCRIPT" fi -# The upstream script defaults TARGET_DIR to SCRIPT_DIR/../lib because it assumes -# it lives in a scripts/ subdir. Override to put things in lib/ at the project root. -LBUG_TARGET_DIR="$LIB_DIR" bash "$UPSTREAM_SCRIPT" +LBUG_TARGET_DIR="$CACHE_LIB_DIR" LBUG_LIB_KIND="$LIB_KIND" bash "$UPSTREAM_SCRIPT" -# The dylib/so embed a versioned install name (e.g. @rpath/liblbug.0.dylib, -# liblbug.so.0) but the archive only contains the unversioned file. Create a -# symlink so the runtime dynamic linker can resolve the name it expects. OS="$(uname -s)" -case "$OS" in - Darwin) - if [ ! -e "$LIB_DIR/liblbug.0.dylib" ]; then - ln -s liblbug.dylib "$LIB_DIR/liblbug.0.dylib" - echo "Created symlink liblbug.0.dylib -> liblbug.dylib" - fi - ;; - Linux) - if [ ! -e "$LIB_DIR/liblbug.so.0" ]; then - ln -s liblbug.so "$LIB_DIR/liblbug.so.0" - echo "Created symlink liblbug.so.0 -> liblbug.so" - fi - ;; -esac - -# Copy the header to the project root so it is available without a -I flag. -cp "$LIB_DIR/lbug.h" "$SCRIPT_DIR/lbug.h" -echo "Copied lbug.h to $SCRIPT_DIR" +if [ "$LIB_KIND" = "shared" ]; then + case "$OS" in + Darwin) + LIB_PATH="$CACHE_LIB_DIR/liblbug.dylib" + ;; + Linux) + LIB_PATH="$CACHE_LIB_DIR/liblbug.so" + ;; + MINGW*|MSYS*|CYGWIN*) + LIB_PATH="$CACHE_LIB_DIR/lbug_shared.dll" + ;; + *) + echo "Unsupported OS: $OS" >&2 + exit 1 + ;; + esac +else + case "$OS" in + MINGW*|MSYS*|CYGWIN*) + LIB_PATH="$CACHE_LIB_DIR/lbug.lib" + ;; + *) + LIB_PATH="$CACHE_LIB_DIR/liblbug.a" + ;; + esac +fi + +if [ ! -f "$LIB_PATH" ]; then + echo "Expected precompiled library not found at $LIB_PATH" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$ENV_FILE")" +if [ "$LIB_KIND" = "shared" ]; then + cat > "$ENV_FILE" < "$ENV_FILE" < Date: Fri, 29 May 2026 10:42:54 -0700 Subject: [PATCH 4/7] Improve latest version parsing --- download-liblbug.sh | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/download-liblbug.sh b/download-liblbug.sh index 132904c..9d74e10 100755 --- a/download-liblbug.sh +++ b/download-liblbug.sh @@ -97,15 +97,22 @@ TMPDIR="$(mktemp -d)" trap 'rm -rf "$TMPDIR"' EXIT fetch_release_archive() { - local version + local download_url if [ -n "$VERSION_OVERRIDE" ]; then + local version version="$VERSION_OVERRIDE" + version="${version#v}" + if [ -z "$version" ]; then + echo "LBUG_VERSION must not be empty" >&2 + exit 1 + fi + download_url="https://github.com/${REPOSITORY}/releases/download/v${version}/${ARCHIVE}" + SOURCE_DESC="release:v${version}" else - version="$(curl -sS "https://api.github.com/repos/${REPOSITORY}/releases/latest" | grep -o '"tag_name": "v\([^"]*\)"' | cut -d'"' -f4 | cut -c2-)" + download_url="https://github.com/${REPOSITORY}/releases/latest/download/${ARCHIVE}" + SOURCE_DESC="release:latest" fi - local download_url="https://github.com/${REPOSITORY}/releases/download/v${version}/${ARCHIVE}" curl -fSL "$download_url" -o "$TMPDIR/$ARCHIVE" - echo "release:v${version}" } fetch_run_artifact() { @@ -121,13 +128,13 @@ fetch_run_artifact() { exit 1 fi mv "$extracted_archive" "$TMPDIR/$ARCHIVE" - echo "run:${RUN_ID}/${ARTIFACT_NAME}" + SOURCE_DESC="run:${RUN_ID}/${ARTIFACT_NAME}" } if [ -n "$RUN_ID" ]; then - SOURCE_DESC="$(fetch_run_artifact)" + fetch_run_artifact else - SOURCE_DESC="$(fetch_release_archive)" + fetch_release_archive fi if [[ "$ARCHIVE" == *.zip ]]; then From b9e7e08af0164fe7ab70f4599cba91933fb6a80f Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 29 May 2026 10:45:00 -0700 Subject: [PATCH 5/7] Fix go build --- download_lbug.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/download_lbug.sh b/download_lbug.sh index 85b3ee2..939b482 100755 --- a/download_lbug.sh +++ b/download_lbug.sh @@ -1,13 +1,13 @@ #!/bin/sh # Wrapper around upstream download-liblbug.sh (same pattern as go-ladybug). -# Downloads prebuilt liblbug into a local cache and writes CMake env flags. +# Downloads prebuilt liblbug into the local cgo library directory. set -eu SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" ENV_FILE="${1:-$PROJECT_DIR/.cache/lbug-prebuilt.env}" -CACHE_LIB_DIR="${LBUG_TARGET_DIR:-$PROJECT_DIR/.cache/lbug-prebuilt/lib}" +CACHE_LIB_DIR="${LBUG_TARGET_DIR:-$SCRIPT_DIR/lib}" LIB_KIND="${LBUG_LIB_KIND:-static}" UPSTREAM_SCRIPT="$SCRIPT_DIR/download-liblbug.sh" UPSTREAM_URL="https://raw.githubusercontent.com/LadybugDB/ladybug/refs/heads/main/scripts/download-liblbug.sh" From 79e7d33fb42de5ffeebcf2f007c85618bd1e05ab Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 29 May 2026 10:57:23 -0700 Subject: [PATCH 6/7] Fix bundled liblbug linker flags --- cgo_bundled.go | 2 +- download_lbug.sh | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cgo_bundled.go b/cgo_bundled.go index 7db4d52..cf58bee 100644 --- a/cgo_bundled.go +++ b/cgo_bundled.go @@ -7,7 +7,7 @@ package lbug /* #cgo CFLAGS: -I${SRCDIR}/lib #cgo darwin LDFLAGS: -lc++ -L${SRCDIR}/lib -llbug -Wl,-rpath,${SRCDIR}/lib -#cgo linux LDFLAGS: -L${SRCDIR}/lib -llbug -Wl,-rpath,${SRCDIR}/lib +#cgo linux LDFLAGS: -L${SRCDIR}/lib -llbug -lstdc++ -Wl,-rpath,${SRCDIR}/lib #cgo windows LDFLAGS: -L${SRCDIR}/lib -llbug_shared #include "lbug.h" */ diff --git a/download_lbug.sh b/download_lbug.sh index 939b482..6d81ea3 100755 --- a/download_lbug.sh +++ b/download_lbug.sh @@ -8,10 +8,20 @@ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" ENV_FILE="${1:-$PROJECT_DIR/.cache/lbug-prebuilt.env}" CACHE_LIB_DIR="${LBUG_TARGET_DIR:-$SCRIPT_DIR/lib}" -LIB_KIND="${LBUG_LIB_KIND:-static}" UPSTREAM_SCRIPT="$SCRIPT_DIR/download-liblbug.sh" UPSTREAM_URL="https://raw.githubusercontent.com/LadybugDB/ladybug/refs/heads/main/scripts/download-liblbug.sh" +OS="$(uname -s)" +case "$OS" in + MINGW*|MSYS*|CYGWIN*) + DEFAULT_LIB_KIND="shared" + ;; + *) + DEFAULT_LIB_KIND="static" + ;; +esac +LIB_KIND="${LBUG_LIB_KIND:-$DEFAULT_LIB_KIND}" + # Fetch the upstream helper if needed. if [ ! -f "$UPSTREAM_SCRIPT" ]; then echo "Fetching $UPSTREAM_URL ..." @@ -21,7 +31,6 @@ fi LBUG_TARGET_DIR="$CACHE_LIB_DIR" LBUG_LIB_KIND="$LIB_KIND" bash "$UPSTREAM_SCRIPT" -OS="$(uname -s)" if [ "$LIB_KIND" = "shared" ]; then case "$OS" in Darwin) From bb52009ee8a67a689f515084abee20d23e24511a Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 29 May 2026 11:00:28 -0700 Subject: [PATCH 7/7] Link libm for bundled liblbug on Linux --- cgo_bundled.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cgo_bundled.go b/cgo_bundled.go index cf58bee..1c1aa3b 100644 --- a/cgo_bundled.go +++ b/cgo_bundled.go @@ -7,7 +7,7 @@ package lbug /* #cgo CFLAGS: -I${SRCDIR}/lib #cgo darwin LDFLAGS: -lc++ -L${SRCDIR}/lib -llbug -Wl,-rpath,${SRCDIR}/lib -#cgo linux LDFLAGS: -L${SRCDIR}/lib -llbug -lstdc++ -Wl,-rpath,${SRCDIR}/lib +#cgo linux LDFLAGS: -L${SRCDIR}/lib -llbug -lstdc++ -lm -Wl,-rpath,${SRCDIR}/lib #cgo windows LDFLAGS: -L${SRCDIR}/lib -llbug_shared #include "lbug.h" */