Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dist/
# testing & linting
.pytest_cache/
.ruff_cache/
.uv-cache/
.mypy_cache/
.coverage
htmlcov/
Expand Down Expand Up @@ -60,9 +61,13 @@ GEMINI.md
QWEN.md
.omx/
.opencode/
docs/plans
CODEBUDDY.md
.codegraph/
.mimocode/
.grok/
.reasonix/
.mcp.json

# go core binary
swanlab/bin/
19 changes: 18 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
AGENTS_SKILL_DIR := .agents/skills
SKILLS_DIR := docs/skills

.PHONY: init sync format proto unit bench clean build publish backport link-skills unlink-skills relink-skills core-lint core-fmt core-test core-build core-tidy
.PHONY: init sync format proto unit bench clean build publish backport link-skills unlink-skills relink-skills core-lint core-fmt core-test core-build core-tidy core-bin release-build release-verify

# ----------------------------------
# SKILL (docs/skills)
Expand Down Expand Up @@ -58,6 +58,7 @@ clean:
# ----------------------------------
# Python package (swanlab/)
# ----------------------------------
.PHONY: init sync format proto unit bench clean build publish link-skills unlink-skills relink-skills

init:
uv sync --all-extras
Expand Down Expand Up @@ -110,3 +111,19 @@ core-build:

core-tidy:
cd core && go mod tidy

# ----------------------------------
# Release (scripts/build_release.sh)
# ----------------------------------

# 本机平台编译 swanlab-core 到 swanlab/bin/,供日常联调(复用发布构建参数)
core-bin:
python3 core/hatch.py

# 完整发布构建:sdist + any 兜底 wheel + 6 平台 wheel + 校验,全部进 dist/
release-build:
bash scripts/build_release.sh $(VERSION)

# 仅校验 dist/ 中已有产物(twine check + 结构完整性)
release-verify:
bash scripts/build_release.sh --verify-only
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ import swanlab
# 初始化一个新的swanlab实验
swanlab.init(
project="my-first-ml",
config={'learning-rate': 0.003},
config={"learning-rate": 0.003},
)

# 记录指标
Expand Down
2 changes: 1 addition & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ import swanlab
# Initialize a new SwanLab experiment
swanlab.init(
project="my-first-ml",
config={'learning-rate': 0.003},
config={"learning-rate": 0.003},
)

# Log metrics
Expand Down
2 changes: 1 addition & 1 deletion README_JP.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ import swanlab
# 新しいSwanLab実験を初期化
swanlab.init(
project="my-first-ml",
config={'learning-rate': 0.003},
config={"learning-rate": 0.003},
)

# 指標を記録
Expand Down
2 changes: 1 addition & 1 deletion README_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ import swanlab
# Инициализация нового эксперимента SwanLab
swanlab.init(
project="my-first-ml",
config={'learning-rate': 0.003},
config={"learning-rate": 0.003},
)

# Запись метрик
Expand Down
164 changes: 164 additions & 0 deletions core/cmd/swanlab-core/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Command swanlab-core 是 SwanLab Go core 的进程入口。
//
// 当前为发布脚手架阶段:仅建立进程生命周期骨架——版本上报(--version)、
// 端点监听、父进程退出监控与信号处理;gRPC 服务端(CoreService /
// CoreSyncService / ProbeService)在后续迭代中接入,届时替换 serve 循环。
//
// 端点约定:
//
// --listen unix:///path/to/uds Linux/macOS 进程内通信(默认路径由 Python SDK 分配)
// --listen tcp://127.0.0.1:port Windows 回环地址(uds 不可用,named pipe 支持后续提供)
//
// 生命周期:父进程退出(process 包监控)或收到 SIGINT/SIGTERM 时优雅退出,
// 防止 Python SDK 崩溃后 core 沦为孤儿进程。
package main

import (
"context"
"errors"
"flag"
"fmt"
"net"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"syscall"

"github.com/swanhubx/swanlab/core/internal/pkg/console"
"github.com/swanhubx/swanlab/core/internal/pkg/process"
)

// version 与 commit 由构建管线通过 -ldflags -X 注入(见 core/hatch.py),
// 缺省值仅供本地 go run / go build 使用。
var (
version = "dev"
commit = "unknown"
)

// 与命令行参数等价的环境变量,供 Python SDK spawn 时注入。
const (
envListenAddr = "SWANLAB_CORE_LISTEN"
envParentPID = "SWANLAB_CORE_PARENT_PID"
)

// 进程退出码约定:0 正常退出(含信号触发的优雅关闭);2 用法错误;1 运行错误。
const (
exitUsageError = 2
exitRunError = 1
)

func main() {
os.Exit(run(os.Args[1:]))
}

func run(args []string) int {
fs := flag.NewFlagSet("swanlab-core", flag.ContinueOnError)
printVersion := fs.Bool("version", false, "打印版本信息后退出")
listenAddr := fs.String("listen", os.Getenv(envListenAddr),
"监听端点,格式 unix://<uds路径> 或 tcp://<地址:端口>;Windows 仅支持 tcp:// 回环地址")
parentPID := fs.Int("parent-pid", envInt(envParentPID),
"预期父进程 PID,父进程退出时 core 随之退出;未指定时取启动瞬间的实际父进程")
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return exitUsageError
}

if *printVersion {
fmt.Printf("swanlab-core %s (commit %s)\n", version, commit)
return 0
}
if *listenAddr == "" {
console.Error("未指定监听端点:通过 --listen 或环境变量 " + envListenAddr + " 传入")
return exitUsageError
}

ln, err := listen(*listenAddr)
if err != nil {
console.Error("监听失败:", err)
return exitRunError
}
defer ln.Close()

// 父进程监控:显式传入的 PID 优先(Python SDK 启动约定),未传时回退为
// 监控启动瞬间的实际父进程(本地终端运行场景)。监控建立失败按约定终止启动。
pid := *parentPID
if pid <= 0 {
pid = os.Getppid()
}
parentExited, err := process.NotifyOnParentExit(pid)
if err != nil {
console.Error("父进程监控建立失败,终止启动:", err)
return exitRunError
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

console.Infof("swanlab-core %s listening on %s (parent pid %d)", version, *listenAddr, pid)

serveErr := make(chan error, 1)
go func() {
serveErr <- serve(ln)
}()

select {
case <-ctx.Done():
console.Info("收到退出信号,正在关闭")
case <-parentExited:
console.Warning("父进程已退出,core 随之退出")
case err := <-serveErr:
if err != nil {
console.Error("监听异常退出:", err)
return exitRunError
}
}
return 0
}

// listen 按协议前缀创建监听器。uds 仅在非 Windows 平台可用;Windows 使用
// TCP 回环地址兜底(named pipe 接入后在此分支扩展)。
func listen(addr string) (net.Listener, error) {
scheme, rest, ok := strings.Cut(addr, "://")
if !ok {
return nil, fmt.Errorf("监听端点缺少协议前缀(unix:// 或 tcp://): %s", addr)
}
switch scheme {
case "unix":
if runtime.GOOS == "windows" {
return nil, errors.New("windows 平台不支持 unix:// 端点,请使用 tcp://127.0.0.1:<端口>")
}
return net.Listen("unix", rest)
case "tcp":
return net.Listen("tcp", rest)
default:
return nil, fmt.Errorf("不支持的监听协议 %q(仅 unix:// 或 tcp://)", scheme)
}
}

// serve 接受连接后立即关闭。脚手架阶段仅验证端点连通性,
// gRPC 服务端就绪后由此接入 serve 逻辑。
func serve(ln net.Listener) error {
for {
conn, err := ln.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return nil
}
return err
}
_ = conn.Close()
}
}

// envInt 解析整型环境变量,缺失或非法时返回 0。
func envInt(name string) int {
v, err := strconv.Atoi(strings.TrimSpace(os.Getenv(name)))
if err != nil {
return 0
}
return v
}
106 changes: 106 additions & 0 deletions core/hatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""swanlab-core Go 模块编译封装。

供两处调用:

- ``hatch_build.py`` 构建钩子(发布/平台 wheel 构建)
- 直接执行 ``python3 core/hatch.py``(``make core-bin``,本机构建)

职责单一:以 ``core/`` 为工作目录执行 ``go build``,将产物输出到仓库根的
``swanlab/bin/swanlab-core(.exe)``,并注入版本号与 commit。

注:``CGO_ENABLED=0`` 静态编译下,Go 内部链接器写入的 ELF ``.gnu.version`` /
``.gnu.version_r`` section 会导致 auditwheel 崩溃。仅当未来引入 manylinux
容器认证(cibuildwheel 路线)时,才需要在构建流程中用 objcopy 移除这两个
section;当前自声明 manylinux tag 的构建不涉及 auditwheel,无需处理。
"""

from __future__ import annotations

import json
import os
import pathlib
import subprocess

_REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
_CORE_DIR = _REPO_ROOT / "core"
_ENTRY_PACKAGE = "./cmd/swanlab-core"


def build_core(
go_binary: pathlib.Path,
output_path: pathlib.PurePath,
target_system: str | None = None,
target_arch: str | None = None,
) -> None:
"""编译 swanlab-core。

Args:
go_binary: go 可执行文件路径,必须存在。
output_path: 产物路径,相对仓库根(如 ``swanlab/bin/swanlab-core``)。
target_system: 目标 GOOS,``None`` 表示使用本机平台。
target_arch: 目标 GOARCH,``None`` 表示使用本机平台。
"""
version = _package_version()
commit = _git_commit()
# -s -w 移除符号表与 DWARF 调试信息;-X 将版本信息注入 main 包变量
ldflags = f"-s -w -X main.version={version} -X main.commit={commit}"
# go build 以 core/ 为工作目录,因此输出路径需要相对 core/ 前移一级
output = pathlib.Path("..") / output_path

subprocess.check_call(
[
str(go_binary),
"build",
"-trimpath",
f"-ldflags={ldflags}",
"-o",
str(output),
_ENTRY_PACKAGE,
],
cwd=str(_CORE_DIR),
env=_go_env(target_system, target_arch),
)

if not _is_windows_target(target_system):
# 显式设置可执行位:wheel 以 zip 外部属性记录权限,pip 解压时还原
os.chmod(_REPO_ROOT / output_path, 0o755)


def _go_env(target_system: str | None, target_arch: str | None) -> dict[str, str]:
env = os.environ.copy()
if target_system:
env["GOOS"] = target_system
if target_arch:
env["GOARCH"] = target_arch
# 纯 Go 无 cgo:静态编译,保证全平台交叉编译无宿主依赖
env["CGO_ENABLED"] = "0"
return env


def _is_windows_target(target_system: str | None) -> bool:
if target_system:
return target_system == "windows"
return os.name == "nt"


def _package_version() -> str:
"""从 swanlab/package.json 读取版本号,与 SDK 版本保持同源。"""
package_json = _REPO_ROOT / "swanlab" / "package.json"
version = json.loads(package_json.read_text(encoding="utf-8"))["version"]
return str(version)


def _git_commit() -> str:
"""返回当前 commit SHA;不在 git 仓库中或 git 不可用时返回 ``unknown``。"""
try:
return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=str(_REPO_ROOT), text=True).strip()
except Exception:
return "unknown"


if __name__ == "__main__":
# 本机构建入口(make core-bin),复用与发布构建一致的编译参数
exe = "swanlab-core.exe" if os.name == "nt" else "swanlab-core"
go = pathlib.Path(os.environ.get("GO", "go"))
build_core(go_binary=go, output_path=pathlib.PurePath("swanlab", "bin", exe))
print(f"swanlab/bin/{exe} built for host platform")
Loading
Loading