Skip to content

Commit 23b1ff7

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat: split capi/idiomatic output by API level with build tags
Teach capigen and idiomgen to split functions by API level. Functions above the base level (35) go into separate files with //go:build android_ndk{N} tags. For API-36 functions (NdkMediaCodecInfo.h), the cgo preamble uses #undef/__define __ANDROID_MIN_SDK_VERSION__ 36 and __ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ to make declarations visible despite compiling with an API-35 target NDK. This fixes CI: without -tags android_ndk36, the 40 API-36 media functions are excluded. With the tag, they're included. The solution is generic — any module can use api_levels in its overlay.
1 parent e123822 commit 23b1ff7

9 files changed

Lines changed: 1107 additions & 451 deletions

File tree

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,14 @@ capi:
4141
spec="spec/generated/$$m.yaml"; \
4242
[ -f "$$manifest" ] || continue; \
4343
[ -f "$$spec" ] || continue; \
44+
overlay="spec/overlays/$$m.yaml"; \
45+
overlay_flag=""; \
46+
[ -f "$$overlay" ] && overlay_flag="-overlay $$overlay"; \
4447
echo "capigen $$m"; \
4548
go run ./tools/cmd/capigen \
4649
-spec "$$spec" \
4750
-manifest "$$manifest" \
51+
$$overlay_flag \
4852
-out "capi/$$m/"; \
4953
done
5054

capi/media/media.go

Lines changed: 0 additions & 444 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

capi/media/media_api36.go

Lines changed: 476 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/overlays/media.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,43 @@ api_levels:
260260
AMediaMuxer_addTrack: 21
261261
AMediaMuxer_start: 21
262262
AMediaMuxer_stop: 21
263+
AMediaCodecInfo_getCanonicalName: 36
264+
AMediaCodecInfo_getKind: 36
265+
AMediaCodecInfo_isVendor: 36
266+
AMediaCodecInfo_getMediaCodecInfoType: 36
267+
AMediaCodecInfo_getMediaType: 36
268+
AMediaCodecInfo_getMaxSupportedInstances: 36
269+
AMediaCodecInfo_isFeatureSupported: 36
270+
AMediaCodecInfo_isFeatureRequired: 36
271+
AMediaCodecInfo_isFormatSupported: 36
272+
AMediaCodecInfo_getAudioCapabilities: 36
273+
AMediaCodecInfo_getVideoCapabilities: 36
274+
AMediaCodecInfo_getEncoderCapabilities: 36
275+
ACodecAudioCapabilities_getBitrateRange: 36
276+
ACodecAudioCapabilities_getSupportedSampleRates: 36
277+
ACodecAudioCapabilities_getSupportedSampleRateRanges: 36
278+
ACodecAudioCapabilities_getMaxInputChannelCount: 36
279+
ACodecAudioCapabilities_getMinInputChannelCount: 36
280+
ACodecAudioCapabilities_getInputChannelCountRanges: 36
281+
ACodecAudioCapabilities_isSampleRateSupported: 36
282+
ACodecPerformancePoint_create: 36
283+
ACodecPerformancePoint_destroy: 36
284+
ACodecPerformancePoint_coversFormat: 36
285+
ACodecPerformancePoint_covers: 36
286+
ACodecPerformancePoint_equals: 36
287+
ACodecVideoCapabilities_getBitrateRange: 36
288+
ACodecVideoCapabilities_getSupportedWidths: 36
289+
ACodecVideoCapabilities_getSupportedHeights: 36
290+
ACodecVideoCapabilities_getWidthAlignment: 36
291+
ACodecVideoCapabilities_getHeightAlignment: 36
292+
ACodecVideoCapabilities_getSupportedFrameRates: 36
293+
ACodecVideoCapabilities_getSupportedWidthsFor: 36
294+
ACodecVideoCapabilities_getSupportedHeightsFor: 36
295+
ACodecVideoCapabilities_getSupportedFrameRatesFor: 36
296+
ACodecVideoCapabilities_getAchievableFrameRatesFor: 36
297+
ACodecVideoCapabilities_getNextSupportedPerformancePoint: 36
298+
ACodecVideoCapabilities_areSizeAndRateSupported: 36
299+
ACodecVideoCapabilities_isSizeSupported: 36
300+
ACodecEncoderCapabilities_getQualityRange: 36
301+
ACodecEncoderCapabilities_getComplexityRange: 36
302+
ACodecEncoderCapabilities_isBitrateModeSupported: 36

tools/cmd/capigen/main.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ func main() {
1616
var (
1717
specPath = flag.String("spec", "", "path to spec YAML file (required)")
1818
manifestPath = flag.String("manifest", "", "path to capi manifest YAML (required)")
19+
overlayPath = flag.String("overlay", "", "path to overlay YAML (optional, for api_levels)")
1920
outDir = flag.String("out", "", "output directory for generated package (required)")
2021
)
2122
flag.Parse()
2223

2324
if *specPath == "" || *manifestPath == "" || *outDir == "" {
24-
fmt.Fprintln(os.Stderr, "usage: capigen -spec <spec.yaml> -manifest <manifest.yaml> -out <dir>")
25+
fmt.Fprintln(os.Stderr, "usage: capigen -spec <spec.yaml> -manifest <manifest.yaml> -out <dir> [-overlay <overlay.yaml>]")
2526
os.Exit(1)
2627
}
2728

@@ -37,7 +38,16 @@ func main() {
3738
os.Exit(1)
3839
}
3940

40-
if err := capigen.GeneratePackage(spec, manifest, *outDir); err != nil {
41+
var apiLevels map[string]int
42+
if *overlayPath != "" {
43+
apiLevels, err = readAPILevels(*overlayPath)
44+
if err != nil {
45+
fmt.Fprintf(os.Stderr, "read overlay: %v\n", err)
46+
os.Exit(1)
47+
}
48+
}
49+
50+
if err := capigen.GeneratePackage(spec, manifest, *outDir, apiLevels); err != nil {
4151
fmt.Fprintf(os.Stderr, "generate: %v\n", err)
4252
os.Exit(1)
4353
}
@@ -73,3 +83,20 @@ func readManifest(path string) (*capigen.Manifest, error) {
7383

7484
return &m, nil
7585
}
86+
87+
// readAPILevels reads only the api_levels map from an overlay YAML file.
88+
func readAPILevels(path string) (map[string]int, error) {
89+
data, err := os.ReadFile(path)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
var overlay struct {
95+
APILevels map[string]int `yaml:"api_levels"`
96+
}
97+
if err := yaml.Unmarshal(data, &overlay); err != nil {
98+
return nil, err
99+
}
100+
101+
return overlay.APILevels, nil
102+
}

tools/pkg/capigen/generate.go

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,23 @@ type FlagGroup struct {
8686
Flags []string `yaml:"flags"`
8787
}
8888

89+
// BaseAPILevel is the default Android API level. Functions at or below this
90+
// level go into the main generated file; functions above it are emitted into
91+
// separate build-tagged files.
92+
const BaseAPILevel = 35
93+
8994
// GeneratePackage generates a complete capi/ Go package from the spec
9095
// and manifest. It writes doc.go, types.go, const.go, cgo_helpers.go,
9196
// cgo_helpers.h, and {module}.go into outDir.
97+
//
98+
// apiLevels is an optional map from C function name to the minimum Android
99+
// API level required. Functions above BaseAPILevel are placed in separate
100+
// files with //go:build android_ndk{N} tags.
92101
func GeneratePackage(
93102
spec *specmodel.Spec,
94103
manifest *Manifest,
95104
outDir string,
105+
apiLevels ...map[string]int,
96106
) error {
97107
if err := os.MkdirAll(outDir, 0o755); err != nil {
98108
return fmt.Errorf("creating output directory %s: %w", outDir, err)
@@ -132,14 +142,51 @@ func GeneratePackage(
132142
// the same name, so the type gets a "_s" suffix.
133143
typeRenameMap := computeTypeRenameMap(spec, structPrefixSet)
134144

145+
// Resolve API levels map.
146+
var levels map[string]int
147+
if len(apiLevels) > 0 && apiLevels[0] != nil {
148+
levels = apiLevels[0]
149+
}
150+
151+
// Split functions into base (no tag) and per-API-level groups.
152+
baseFunctions := make(map[string]specmodel.FuncDef)
153+
higherFunctions := make(map[int]map[string]specmodel.FuncDef) // api level -> functions
154+
for name, fn := range spec.Functions {
155+
level := levels[name]
156+
if level > BaseAPILevel {
157+
if higherFunctions[level] == nil {
158+
higherFunctions[level] = make(map[string]specmodel.FuncDef)
159+
}
160+
higherFunctions[level][name] = fn
161+
} else {
162+
baseFunctions[name] = fn
163+
}
164+
}
165+
166+
// Build a base spec (without higher-API functions) for the main file.
167+
baseSpec := *spec
168+
baseSpec.Functions = baseFunctions
169+
135170
files := map[string]string{}
136171
files["doc.go"] = generateDocGo(pkgName, manifest.Generator.PackageDescription)
137172
files["types.go"] = generateTypesGo(pkgName, preamble, spec, callbackSet, structPrefixSet, typeRenameMap)
138173
files["const.go"] = generateConstGo(pkgName, preamble, spec, enumTypedefSet)
139174
files["cgo_helpers.go"] = generateCgoHelpersGo(pkgName, preamble, spec, callbackSet)
140175
files["cgo_helpers.h"] = generateCgoHelpersH(pkgName, manifest, spec, structPrefixSet)
141176
files["cgo_helpers.c"] = generateCgoHelpersC(pkgName, spec, structPrefixSet)
142-
files[pkgName+".go"] = generateFunctionsGo(pkgName, preamble, spec, callbackSet, structPrefixSet, typeRenameMap)
177+
files[pkgName+".go"] = generateFunctionsGo(pkgName, preamble, &baseSpec, callbackSet, structPrefixSet, typeRenameMap)
178+
179+
// Generate per-API-level function files with build tags.
180+
apiLevelKeys := sortedIntKeys(higherFunctions)
181+
for _, level := range apiLevelKeys {
182+
funcs := higherFunctions[level]
183+
levelSpec := *spec
184+
levelSpec.Functions = funcs
185+
fileName := fmt.Sprintf("%s_api%d.go", pkgName, level)
186+
levelPreamble := buildAPILevelPreamble(manifest, level)
187+
buildTag := fmt.Sprintf("//go:build android_ndk%d\n\n", level)
188+
files[fileName] = buildTag + generateFunctionsGo(pkgName, levelPreamble, &levelSpec, callbackSet, structPrefixSet, typeRenameMap)
189+
}
143190

144191
for name, content := range files {
145192
path := filepath.Join(outDir, name)
@@ -181,6 +228,46 @@ func buildCGoPreamble(manifest *Manifest) string {
181228
return sb.String()
182229
}
183230

231+
// buildAPILevelPreamble constructs a CGo comment block for a higher API level.
232+
// It uses #undef + #define __ANDROID_MIN_SDK_VERSION__ before the includes
233+
// so the preprocessor exposes declarations guarded by __INTRODUCED_IN(N).
234+
// The #undef is necessary because the compiler's target triple (e.g.
235+
// android35-clang) pre-defines __ANDROID_MIN_SDK_VERSION__ to the base level.
236+
// __ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ disables the strict availability
237+
// attribute that would otherwise cause Clang to reject the declarations even
238+
// when __BIONIC_AVAILABILITY_GUARD passes.
239+
func buildAPILevelPreamble(manifest *Manifest, apiLevel int) string {
240+
var sb strings.Builder
241+
sb.WriteString("/*\n")
242+
for _, fg := range manifest.Generator.FlagGroups {
243+
sb.WriteString("#cgo ")
244+
sb.WriteString(fg.Name)
245+
sb.WriteString(": ")
246+
sb.WriteString(strings.Join(fg.Flags, " "))
247+
sb.WriteString("\n")
248+
}
249+
sb.WriteString("#undef __ANDROID_MIN_SDK_VERSION__\n")
250+
fmt.Fprintf(&sb, "#define __ANDROID_MIN_SDK_VERSION__ %d\n", apiLevel)
251+
sb.WriteString("#define __ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ 1\n")
252+
for _, inc := range manifest.Generator.Includes {
253+
fmt.Fprintf(&sb, "#include \"%s\"\n", inc)
254+
}
255+
sb.WriteString("#include <stdlib.h>\n")
256+
sb.WriteString("#include \"cgo_helpers.h\"\n")
257+
sb.WriteString("*/")
258+
return sb.String()
259+
}
260+
261+
// sortedIntKeys returns sorted keys from a map[int]V.
262+
func sortedIntKeys[V any](m map[int]V) []int {
263+
keys := make([]int, 0, len(m))
264+
for k := range m {
265+
keys = append(keys, k)
266+
}
267+
sort.Ints(keys)
268+
return keys
269+
}
270+
184271
// buildEnumTypedefSet returns the set of type names that have matching enum groups.
185272
func buildEnumTypedefSet(spec *specmodel.Spec) map[string]bool {
186273
set := make(map[string]bool)

tools/pkg/capigen/generate_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,86 @@ func TestGeneratePackage(t *testing.T) {
107107
}
108108
}
109109

110+
func TestGeneratePackageWithAPILevels(t *testing.T) {
111+
spec := looperSpec()
112+
// Add an API-36 function to the spec.
113+
spec.Functions["ALooper_newFeature"] = specmodel.FuncDef{
114+
CName: "ALooper_newFeature",
115+
Params: []specmodel.Param{
116+
{Name: "looper", Type: "*ALooper"},
117+
},
118+
Returns: "int32",
119+
}
120+
manifest := looperManifest()
121+
apiLevels := map[string]int{
122+
"ALooper_newFeature": 36,
123+
}
124+
125+
outDir := t.TempDir()
126+
err := GeneratePackage(spec, manifest, outDir, apiLevels)
127+
require.NoError(t, err)
128+
129+
// Base file should exist and not contain the API-36 function.
130+
basePath := filepath.Join(outDir, "looper.go")
131+
baseContent, err := os.ReadFile(basePath)
132+
require.NoError(t, err)
133+
assert.NotContains(t, string(baseContent), "ALooper_newFeature",
134+
"base file must not contain API-36 function")
135+
assert.Contains(t, string(baseContent), "ALooper_forThread",
136+
"base file must contain base-level functions")
137+
138+
// API-36 file should exist with build tag and the function.
139+
api36Path := filepath.Join(outDir, "looper_api36.go")
140+
api36Content, err := os.ReadFile(api36Path)
141+
require.NoError(t, err)
142+
api36Str := string(api36Content)
143+
assert.Contains(t, api36Str, "//go:build android_ndk36",
144+
"API-36 file must have build tag")
145+
assert.Contains(t, api36Str, "ALooper_newFeature",
146+
"API-36 file must contain the higher-API function")
147+
assert.Contains(t, api36Str, "#undef __ANDROID_MIN_SDK_VERSION__",
148+
"API-36 preamble must undef the SDK version")
149+
assert.Contains(t, api36Str, "#define __ANDROID_MIN_SDK_VERSION__ 36",
150+
"API-36 preamble must define SDK version 36")
151+
assert.Contains(t, api36Str, "__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__",
152+
"API-36 preamble must enable weak symbols")
153+
}
154+
155+
func TestGeneratePackageWithoutAPILevels(t *testing.T) {
156+
spec := looperSpec()
157+
manifest := looperManifest()
158+
159+
outDir := t.TempDir()
160+
// No API levels — should produce the same output as before.
161+
err := GeneratePackage(spec, manifest, outDir)
162+
require.NoError(t, err)
163+
164+
basePath := filepath.Join(outDir, "looper.go")
165+
baseContent, err := os.ReadFile(basePath)
166+
require.NoError(t, err)
167+
assert.Contains(t, string(baseContent), "ALooper_forThread")
168+
assert.Contains(t, string(baseContent), "ALooper_addFd")
169+
170+
// No API-level files should be generated.
171+
entries, err := os.ReadDir(outDir)
172+
require.NoError(t, err)
173+
for _, e := range entries {
174+
assert.NotContains(t, e.Name(), "_api",
175+
"no API-level files should exist without API levels")
176+
}
177+
}
178+
179+
func TestBuildAPILevelPreamble(t *testing.T) {
180+
manifest := looperManifest()
181+
preamble := buildAPILevelPreamble(manifest, 36)
182+
183+
assert.Contains(t, preamble, "#undef __ANDROID_MIN_SDK_VERSION__")
184+
assert.Contains(t, preamble, "#define __ANDROID_MIN_SDK_VERSION__ 36")
185+
assert.Contains(t, preamble, "#define __ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ 1")
186+
assert.Contains(t, preamble, "#cgo LDFLAGS: -landroid")
187+
assert.Contains(t, preamble, "#include \"android/looper.h\"")
188+
}
189+
110190
func TestGenerateDocGo(t *testing.T) {
111191
content := generateDocGo("looper", "Raw CGo bindings for Android looper")
112192
assert.Contains(t, content, "package looper")

0 commit comments

Comments
 (0)