Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ concurrency:
cancel-in-progress: false

env:
GO_VERSION: "stable"
XCODE_VERSION: "latest-stable"
PYTHON_VERSION: "3.12"
NDK_VERSION: r29

jobs:
Expand Down Expand Up @@ -62,7 +59,8 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
python-version: "3.x"
check-latest: true

- name: Install Python deps
run: |
Expand All @@ -74,7 +72,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
go-version: stable
check-latest: true

- name: Download Go modules
Expand All @@ -83,11 +81,11 @@ jobs:
# =========================
# Apple toolchain
# =========================
- name: Select latest stable Xcode
- name: Select Xcode
if: matrix.target == 'apple'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ env.XCODE_VERSION }}
xcode-version: latest-stable

# =========================
# Android toolchain
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/libxray-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
go-version: stable
check-latest: true
cache-dependency-path: go.sum

- name: Go test
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Validate libXray

on:
push:
branches:
- main
pull_request:

concurrency:
group: validate-libxray-${{ github.ref }}
cancel-in-progress: true

jobs:
go:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v6

- uses: actions/setup-go@v6
with:
go-version: stable
check-latest: true

- name: Download modules
run: go mod download

- name: Vet
run: go vet ./...

- name: Test with race detector
run: go test -race ./... -count=1 -timeout 15m

- name: Build Linux C ABI
run: |
python3 build/main.py linux
grep -q 'CGoFree' linux_so/libXray.h
nm -D linux_so/libXray.so | grep -q ' CGoFree$'

- name: Verify source was restored
run: git diff --exit-code

- name: Check whitespace
run: git diff --check
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,12 @@ The C export is:

```c
char* CGoInvoke(char* requestJSON);
void CGoFree(char* value);
```

`CGoInvoke` allocates its response. The caller must release every non-null
response with `CGoFree`; do not use a platform allocator directly.

The request is a JSON object:

```json
Expand Down
49 changes: 26 additions & 23 deletions build/app/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,29 @@ def before_build(self):
self.prepare_gomobile()

def build(self):
self.before_build()

clean_files = ["libXray-sources.jar", "libXray.aar"]
self.clean_lib_files(clean_files)
os.chdir(self.lib_dir)
# keep same with flutter
ret = subprocess.run(
[
"gomobile",
"bind",
"-target",
"android",
"-androidapi",
"21",
"-ldflags=-checklinkname=0 -extldflags=-Wl,-z,max-page-size=16384",
]
)
if ret.returncode != 0:
raise Exception("build failed")

self.after_build()

self.revert_go_env()
self.snapshot_go_env()
try:
self.before_build()

clean_files = ["libXray-sources.jar", "libXray.aar"]
self.clean_lib_files(clean_files)
os.chdir(self.lib_dir)
# keep same with flutter
ret = subprocess.run(
[
"gomobile",
"bind",
"-target",
"android",
"-androidapi",
"21",
"-ldflags=-checklinkname=0 -extldflags=-Wl,-z,max-page-size=16384",
]
)
if ret.returncode != 0:
raise Exception("build failed")
finally:
try:
self.after_build()
finally:
self.restore_go_env()
129 changes: 30 additions & 99 deletions build/app/apple_go.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import os.path
import shutil
import subprocess
import random
import time

from app.build import Builder
from app.cmd import create_dir_if_not_exists, delete_dir_if_exists
Expand Down Expand Up @@ -104,32 +102,34 @@ def before_build(self):
self.prepare_static_lib()

def build(self):
self.before_build()
# build ios
self.build_targets(self.ios_targets)
self.merge_static_lib(
self.ios_targets[1].sdk,
[self.ios_targets[1].apple_arch, self.ios_targets[2].apple_arch],
)
# build macos
self.build_targets(self.macos_targets)
self.merge_static_lib(
self.macos_targets[0].sdk,
[self.macos_targets[0].apple_arch, self.macos_targets[1].apple_arch],
)
# build tvos
self.build_targets(self.tvos_targets)
self.merge_static_lib(
self.tvos_targets[1].sdk,
[self.tvos_targets[1].apple_arch, self.tvos_targets[2].apple_arch],
)

self.after_build()

self.create_include_dir()
self.create_framework()

self.revert_go_env()
self.snapshot_go_env()
try:
self.before_build()
# build ios
self.build_targets(self.ios_targets)
self.merge_static_lib(
self.ios_targets[1].sdk,
[self.ios_targets[1].apple_arch, self.ios_targets[2].apple_arch],
)
# build macos
self.build_targets(self.macos_targets)
self.merge_static_lib(
self.macos_targets[0].sdk,
[self.macos_targets[0].apple_arch, self.macos_targets[1].apple_arch],
)
# build tvos
self.build_targets(self.tvos_targets)
self.merge_static_lib(
self.tvos_targets[1].sdk,
[self.tvos_targets[1].apple_arch, self.tvos_targets[2].apple_arch],
)
self.create_include_dir()
self.create_framework()
finally:
try:
self.after_build()
finally:
self.restore_go_env()

def build_targets(self, targets: list[AppleTarget]):
for target in targets:
Expand Down Expand Up @@ -162,62 +162,17 @@ def run_build_cmd(
run_env["CGO_ENABLED"] = "1"
run_env["DARWIN_SDK"] = sdk

# Enhanced obfuscation for uniqueness of each build
ldflags = "-s -w" # Strip symbol table and DWARF debug info

# Add build timestamp with microseconds for greater uniqueness
build_timestamp = time.strftime("%Y%m%d%H%M%S") + str(
int(time.time() * 1000) % 1000
)
ldflags += f" -X 'main.buildTime={build_timestamp}'"

# Add random identifier for each build
random_id = "".join(
random.choices(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", k=16
)
)
ldflags += f" -X 'main.buildID={random_id}'"

# Add git commit hash if available
try:
git_hash = (
subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL
)
.decode()
.strip()
)
ldflags += f" -X 'main.buildVersion={git_hash}'"
except:
# If git is not available, use a random hash
fake_hash = "".join(random.choices("0123456789abcdef", k=7))
ldflags += f" -X 'main.buildVersion={fake_hash}'"

# Add random unused variables to change binary fingerprint
for i in range(5):
var_name = f"unusedVar{i}"
var_value = "".join(
random.choices(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
k=8,
)
)
ldflags += f" -X 'main.{var_name}={var_value}'"

# Use compatible optimization flags
gcflags = "all=-l=4" # Limit inlining but keep other optimizations
ldflags = "-s -w -buildid="

cmd = [
"go",
"build",
"-trimpath", # Remove all file system paths
"-ldflags",
ldflags,
"-gcflags",
gcflags,
f"-o={output_file}",
"-buildmode=c-archive",
self.main_package(),
]
os.chdir(self.lib_dir)
print(cmd)
Expand Down Expand Up @@ -298,27 +253,3 @@ def create_framework(self):
ret = subprocess.run(cmd)
if ret.returncode != 0:
raise Exception(f"create_framework failed")

def after_build(self):
super().after_build()
self.reset_files()

# Modify framework metadata for additional uniqueness
try:
framework_path = os.path.join(self.lib_dir, "LibXray.xcframework")
info_plist = os.path.join(framework_path, "Info.plist")
if os.path.exists(info_plist):
# Add random comment to Info.plist
random_comment = f"<!-- Build ID: {random.randint(10000, 99999)} -->"
with open(info_plist, "r") as f:
content = f.read()

if "<!DOCTYPE plist" in content:
content = content.replace(
"<!DOCTYPE plist", f"{random_comment}\n<!DOCTYPE plist"
)
with open(info_plist, "w") as f:
f.write(content)
except:
# Ignore errors when modifying metadata
pass
39 changes: 21 additions & 18 deletions build/app/apple_gomobile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,25 @@ def before_build(self):
self.prepare_gomobile()

def build(self):
self.before_build()
self.snapshot_go_env()
try:
self.before_build()

os.chdir(self.lib_dir)
ret = subprocess.run(
[
"gomobile",
"bind",
"-target",
"ios,iossimulator,macos,maccatalyst",
"-iosversion",
"15.0",
]
)
if ret.returncode != 0:
raise Exception("build failed")

self.after_build()

self.revert_go_env()
os.chdir(self.lib_dir)
ret = subprocess.run(
[
"gomobile",
"bind",
"-target",
"ios,iossimulator,macos,maccatalyst",
"-iosversion",
"15.0",
]
)
if ret.returncode != 0:
raise Exception("build failed")
finally:
try:
self.after_build()
finally:
self.restore_go_env()
Loading