构建可复用库包
SuperJ 做全程序编译:依赖的源码被编译进你的程序(没有按包的二进制 ABI)。一个"库"就是一个没有 entry 的项目 — 它导出其他项目 import 的类。本教程走一遍创建一个库、通过路径依赖从一个独立项目消费它,以及 git 依赖与工作区替代方案。
1. 搭建库
superj new mathlib
cd mathlib
脚手架生成的 Build.sj 有一个指向 mathlib.Main 的 entry 字段。一个库没有入口点,因此移除 entry 行(或设为 ""):
public class Build {
static final String name = "mathlib";
static final String version = "0.1.0";
static final String spec = "1.0";
static final String[] dependencies = {};
static final String releaseSimd = "sse2";
static final boolean releaseBoundsCheck = true;
static final int releaseOpt = 3;
}
删除脚手架生成的 src/mathlib/Main.sj(库不需要 main 类),然后写库:
rm src/mathlib/Main.sj
// src/mathlib/MathLib.sj
package mathlib;
public class MathLib {
public static int square(int x) {
return x * x;
}
public static int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; i = i + 1) {
result = result * i;
}
return result;
}
public static boolean isPrime(int n) {
if (n < 2) { return false; }
if (n < 4) { return true; }
if (n % 2 == 0) { return false; }
for (int i = 3; i * i <= n; i = i + 2) {
if (n % i == 0) { return false; }
}
return true;
}
}
2. 类型检查库
一个无 entry 的库没有可运行产物 — superj build 对它做类型检查但不链接。用 superj check 验证:
superj check
Checked mathlib (1 source files)
这是库的 CI 门:它编译源码并报告类型错误,但不产出二进制。库已可被消费。
3. 搭建消费者
在一个同级目录:
cd ..
superj new myapp
cd myapp
4. 把库作为路径依赖添加
superj add mathlib ../mathlib
这会编辑 Build.sj 加上依赖:
public class Build {
static final String name = "myapp";
static final String version = "0.1.0";
static final String spec = "1.0";
static final String entry = "myapp.Main";
static final String[] dependencies = { "mathlib", "../mathlib" };
static final String releaseSimd = "sse2";
static final boolean releaseBoundsCheck = true;
static final int releaseOpt = 3;
}
你也可以手编 Build.sj — dependencies 字段是一个 { "name", "path" } 对的扁平数组。
5. 使用库
编辑 src/myapp/Main.sj:
// src/myapp/Main.sj
package myapp;
import mathlib.MathLib;
public class Main {
public static void main(String[] args) {
System.out.println("square(7) = " + MathLib.square(7));
System.out.println("factorial(5) = " + MathLib.factorial(5));
System.out.println("isPrime(17) = " + MathLib.isPrime(17));
}
}
6. 构建并运行
superj run
Built /path/to/myapp/target/debug/myapp
square(7) = 49
factorial(5) = 120
isPrime(17) = true
构建工具把库的源码与你的应用源码一起编译(全程序编译)并链接成一个二进制。要做 release 构建:superj run --release(应用清单的 releaseSimd、releaseBoundsCheck、releaseOpt 字段)。要最大性能:superj build --release --enterprise(全程序 LTO;见 构建系统)。
7. lockfile
首次构建后,消费者项目中出现一个 superj.lock 文件:
# superj.lock — 由 `superj build` 生成;提交此文件。
version = 1
[[package]]
name = "mathlib"
path = "../mathlib"
hash = "c989d08c7e7c3989b7687ace7847e3339b82e0b84f9b4907deb95d0d7fe929f9"
提交 superj.lock — 它把依赖的内容哈希固定下来,用于可复现构建。如果库源码变化,哈希在下次构建时更新,superj.lock 反映新状态。
8. Git 依赖
对于托管在 git 仓库中的库(例如在 Gitea 服务器或本地裸仓库),用 --git 代替路径:
superj add mathlib --git git@example.com:me/mathlib --rev v1.0.0
这会向 Build.sj 加一个 gitDependencies 字段:
static final String[] gitDependencies = { "mathlib", "git@example.com:me/mathlib", "v1.0.0" };
构建工具在首次构建时把固定的提交拉取到内容寻址缓存($SUPERJ_CACHE_DIR,默认 ~/.superj/cache);热缓存可离线构建。lockfile 记录解析后的提交哈希:
[[package]]
name = "mathlib"
source = "git"
url = "git@example.com:me/mathlib"
commit = "a1b2c3d4e5f6..."
hash = "..."
用于测试的本地 git 仓库: 你可以用
file://URL 对一个本地仓库测试 git 依赖流程。把库初始化为 git 仓库(git init && git add -A && git commit -m "init"),然后:superj add mathlib --git file:///path/to/mathlib --rev HEAD。
9. 工作区(monorepo)
当库与消费者位于同一仓库时,用工作区代替路径依赖。创建一个带 workspaceMembers 的根 Build.sj:
myrepo/
Build.sj ← 根清单(工作区)
mathlib/
Build.sj ← 库(无 entry)
src/mathlib/...
myapp/
Build.sj ← 应用(entry = myapp.Main)
src/myapp/...
根 Build.sj:
public class Build {
static final String[] workspaceMembers = { "mathlib", "myapp" };
}
消费者(myapp/Build.sj)仍把库声明为路径依赖(dependencies = { "mathlib", "../mathlib" })。从工作区根:
superj build -p myapp # 只构建 myapp
superj check # 类型检查所有成员
superj run -p myapp # 构建 + 运行 myapp
从成员目录内部执行任何命令,它只作用于该成员,完全像一个独立项目。成员之间作为普通路径依赖相互依赖;一个库成员(无 entry)只做类型检查而非链接。
10. 跨包命名
SuperJ 按全限定名为类符号建立命名空间,因此包不同但简单名相同的类可以共存 — com.a.Foo 与 com.b.Foo,或 com.x.Config 与 SDK 的 sj.util.Config 并存。仍有两样东西会冲突,因为它们共享一个 mangle 后的符号:
- 默认包中的类。 默认(无
package)包中的类没有 FQN 来区分,因此默认包的Config与sj.util.Config冲突。把你的类放进一个包里(superj new生成的脚手架就是这样做的)。 - 完全相同的 FQN 出现两次 — 一个真正的重复声明。
编译器在 superj check 时报告这两种情况,指出违规的全限定名,而不是在链接时才失败。
sj. 包根是保留的,供 SDK 与编译器自身使用:用户代码声明 package sj; 或 package sj.anything; 会在 check/build 时被以一个保留命名空间错误拒绝。这正是让上面的规则滴水不漏的原因 — 一个 SDK 类保持其历史符号名,没有用户类能进入那个命名空间与它冲突。你打包的类可以自由地引用 sj.* 类型(通过 import 或 FQN,照旧);它们只是不能在 sj.* 内被声明。
这在所有构建模式下都成立:两个同简单名的类都从源码编译 — com.a.Foo + com.b.Foo,本地引用或全限定引用 — 在 community/enterprise(--sdk-path)与 VIP(--sdk-source)构建下都能共存并正确分派。
裸名如何解析。 一个裸 Foo 依次表示:当前包的 Foo;然后单类型 import 的 Foo(import com.a.Foo;);然后程序中名为 Foo 的那个类。如果两个或更多包都声明了 Foo 而上述规则都不命中,该引用是一个编译错误,列出每个候选(reference to 'Foo' is ambiguous: com.a.Foo, com.b.Foo — qualify it or add an import)— 绝不静默选择。一个其包不存在的限定名(new com.c.Foo() 而 com.c 不存在)同样是 cannot find class 'com.c.Foo',而不是静默绑定别的 Foo。
一个名为 Main 的库类,如果与消费者的 Main 同在默认包中,仍会冲突 — 按库类的功能命名(MathLib、StringUtils、HttpClient),并给它们一个包。
小结
| 步骤 | 命令 |
|---|---|
| 搭建库 | superj new mylib → 从 Build.sj 移除 entry |
| 类型检查库 | superj check |
| 搭建消费者 | superj new myapp |
| 添加路径依赖 | superj add mylib ../mylib |
| 添加 git 依赖 | superj add mylib --git <url> --rev <rev> |
| 使用库 | import mylib.MyClass; |
| 构建 + 运行 | superj run |
| Release 构建 | superj build --release |
| 最大性能 | superj build --release --enterprise |
| 工作区构建 | superj build -p myapp(从工作区根) |
完整的清单字段参考、feature flag 与构建钩子见 构建系统。