Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2187e3f
Resolve a dependency's condition, and hold the C-family side to a pac…
yume190 Sep 21, 2026
0ecf648
Make a trait something the build decides, the way a configuration is
yume190 Sep 21, 2026
80f9db2
A trait defines its own name, so a trait only ever turns on
yume190 Sep 21, 2026
85af7b9
test(spm): cover each trait selection
yume190 Sep 22, 2026
c0de6be
feat(spm): move listings into Bazel
yume190 Sep 22, 2026
26516be
feat(spm): decide a configuration and a trait selection in the build
yume190 Sep 22, 2026
6b92933
feat(spm): run the program an artifact bundle ships
yume190 Sep 22, 2026
e6ae409
feat(spm): build a package's snippets, and alias a renamed module
yume190 Sep 22, 2026
4dc887b
test(spm): cover binary targets, system libraries and settings
yume190 Sep 22, 2026
6858901
test(spm): cover the C-family shapes and every resource rule
yume190 Sep 22, 2026
3e83ad6
ci(spm): run every fixture, and say what each one holds
yume190 Sep 22, 2026
bd69dde
ci(spm): run on fixture changes, and run the programs a package builds
yume190 Sep 22, 2026
b783069
feat(spm): compile a package in the language mode it declares
yume190 Sep 22, 2026
7736d3f
feat(spm): ask pkg-config where a system library is
yume190 Sep 22, 2026
90e5057
feat(spm): find a package a registry supplied
yume190 Sep 22, 2026
465e5e0
ci(spm): tell the generator where the fixture's pkg-config file is
yume190 Sep 22, 2026
5e5854d
ci: name each matrix lane after what it builds
yume190 Sep 22, 2026
fad9e69
test(spm): pin the language standards, and run XCTest too
yume190 Sep 22, 2026
8fc24fb
test(spm): decide a platform while the rules are written
yume190 Sep 22, 2026
349ab6c
test(spm): run a plugin that belongs to another package
yume190 Sep 22, 2026
f8c550e
test(spm): copy a single file, bundle a test target, loop a link
yume190 Sep 22, 2026
7b421fb
ci(spm): run what SwiftPM does beside what Bazel does
yume190 Sep 22, 2026
d621c32
test(spm): find a target in a conventional root, and reach across a p…
yume190 Sep 22, 2026
7a41430
fix: answer whether a prefix was there at all
yume190 Sep 22, 2026
0411d18
test(spm): fetch the program a binary target ships
yume190 Sep 22, 2026
d5508e4
test(spm): name the package a product belongs to, and what a plugin m…
yume190 Sep 22, 2026
8227a68
test(spm): compile every resource kind, and localize more than once
yume190 Sep 22, 2026
c2166aa
fix(spm): say that a program's resource bundle is not built
yume190 Sep 22, 2026
2760769
ci(spm): build and test every package of a fixture
yume190 Sep 22, 2026
a95a7a8
test(spm): pass swift test the way swift test is run
yume190 Sep 22, 2026
362237d
test(spm): localize with a base and a copied language
yume190 Sep 22, 2026
13fd6c9
feat(spm): pick the localizations a build bundles
yume190 Sep 22, 2026
4bf2e09
test(spm): version the data model, and say why mapping models are not…
yume190 Sep 22, 2026
cdec2f4
docs: record what the SwiftPM side does not do yet
yume190 Sep 22, 2026
869ba38
docs: keep the same record in Chinese
yume190 Sep 22, 2026
92baa2f
fix(xcode): ask the resolved graph which package a product belongs to
yume190 Sep 23, 2026
07e50de
test(spm): a package with no products, and one that is only a plugin
yume190 Sep 23, 2026
d07c4f8
test(spm): expand a macro that belongs to another package
yume190 Sep 23, 2026
477c242
ci(spm): fail a lane on a note no fixture expects
yume190 Sep 23, 2026
55b82e0
fix(spm): find the sources of a package that has no directory for them
yume190 Sep 23, 2026
9190a63
feat: one //:tool for everything a workspace answers or is told
yume190 Sep 23, 2026
3901877
fix: restore generated workspace command labels
yume190 Sep 23, 2026
6edc24e
feat(spm): make plugin runner self-contained
yume190 Sep 23, 2026
666a25e
refactor(cli): one input option, and only the commands a user runs
yume190 Sep 23, 2026
fa4bedd
build(fixture): describe the iOS project, stop tracking the generated…
yume190 Sep 23, 2026
7ef26fd
fix: a static framework is a library, and xcodeproj names targets tha…
yume190 Sep 23, 2026
bba4043
ci(spm): skip a package with nothing to build, name the build system …
yume190 Sep 23, 2026
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
214 changes: 203 additions & 11 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@ on:
paths:
- 'Sources/**'
- 'Tests/**'
- 'spm/**'
- '.github/workflows/**'
- 'Package.swift'
- 'fixture/iOS/**'
- 'mise.toml'
pull_request:
paths:
- 'Sources/**'
- 'Tests/**'
- 'spm/**'
- '.github/workflows/**'
- 'Package.swift'
- 'fixture/iOS/**'
- 'mise.toml'

concurrency:
group: ci
Expand All @@ -27,6 +33,11 @@ jobs:
steps:
- uses: actions/checkout@v6

- uses: jdx/mise-action@v3

- name: Generate iOS fixture
run: tuist generate --path fixture/iOS --no-open

- name: Check Xcode Version
run: |
xcodebuild -version
Expand Down Expand Up @@ -79,6 +90,11 @@ jobs:
steps:
- uses: actions/checkout@v6

- uses: jdx/mise-action@v3

- name: Generate Xcode project
run: tuist generate --path fixture/iOS --no-open

# Every `bazel` in this job reads these: the disk cache holds what the
# build produced, the repository cache what it downloaded. Without the
# second one a runner fetches rules_apple and the rest again every time.
Expand Down Expand Up @@ -114,7 +130,7 @@ jobs:
- name: Bazel Generation
working-directory: fixture/iOS
run: |
../../bazelize --project Example.xcodeproj --output App | tee bazelize.log
../../bazelize --input Example.xcodeproj --output App | tee bazelize.log
! grep -q "did not run the" bazelize.log

- name: Build Application
Expand Down Expand Up @@ -199,6 +215,7 @@ jobs:
# The corpus is not in this repository: `app/` and `spm/` are ignored, so each
# lane clones what it measures at the revision it was measured against.
IntegrateApp:
name: IntegrateApp (${{ matrix.name }})
runs-on: macos-26
needs: [artifact]
timeout-minutes: 120
Expand Down Expand Up @@ -299,7 +316,7 @@ jobs:

- name: Bazel Generation
run: |
./bazelize --project "${{ matrix.name }}/${{ matrix.project }}" --output "${{ matrix.name }}/App" | tee bazelize.log
./bazelize --input "${{ matrix.name }}/${{ matrix.project }}" --output "${{ matrix.name }}/App" | tee bazelize.log
! grep -q "did not run the" bazelize.log

- name: Build Application
Expand All @@ -309,22 +326,64 @@ jobs:
# One lane per SwiftPM fixture in `spm/`: each package is one thing SwiftPM
# can do, and its tests only pass if that thing was generated correctly.
IntegratePackage:
name: IntegratePackage (${{ matrix.name }})
runs-on: macos-26
needs: [artifact]
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- name: ArtifactBundle
plugins: true
# A package rule carries `manual`, so a wildcard build never picks
# one up: a program a package builds is named to be run.
programs: |
//Packages/ArtifactBundle:GreetTool -- /tmp/artifact-bundle.swift
- name: BinaryTarget
- name: BuildToolPlugin
plugins: true
- name: Clang
- name: CommandPlugin
- name: ConfigurationCondition
- name: DependencyCondition
# A trait is a flag, so the build that turns one on is a build to
# test as well: the dependency behind it is linked only then.
config: DependencyCondition.Extras
- name: DependencyShape
- name: Macro
- name: Trait
- name: TargetSources
- name: TargetPath
- name: Platform
- name: PluginDependency
plugins: true
- name: PrebuildPlugin
plugins: true
- name: ProductShapes
programs: |
//Packages/Products:renamed-tool
//Packages/Products:ProductSnippet
- name: RemoteArtifactBundle
plugins: true
- name: RemoteXCFramework
- name: SwiftSettings
- name: SystemLibrary
- name: TargetEmbed
# `.embedInCode` generates nothing under `swiftbuild`, which is
# SwiftPM's own gap rather than anything this package does — and the
# only reason this rule has a package of its own.
swiftpm: --build-system native
- name: TargetExclude
- name: TargetPath
- name: TargetResource
# `native` copies a platform resource rather than compiling it: no
# `momc`, no `actool`, no `ibtool`. Which build system is the
# default moved with the toolchain, so the one that compiles what
# this package is about is named.
swiftpm: --build-system swiftbuild
- name: TargetSources
- name: Trait
traits: Fast,Slow
- name: TraitGraph
traits: Alternative,Leaf,Middle,Top
steps:
- uses: actions/checkout@v6

Expand All @@ -348,19 +407,152 @@ jobs:
with:
name: bazelize

# What the fixture holds bazelize to is what SwiftPM does, so what
# SwiftPM does is run too: a fixture whose two sides disagree is a
# fixture that proves nothing.
#
# Every package of the fixture, not only the one bazelize is pointed at:
# the packages beside it are dependencies with sources of their own, and
# one that stopped building would be found here rather than in whatever
# it broke. A package with no tests is built and not run — there is
# nothing to run — and `App/` is generated, not a package of the fixture.
# A package that is only a plugin has nothing to build either: SwiftPM
# refuses the build rather than doing nothing, so it is skipped by what
# its manifest declares.
- name: SwiftPM Build And Test
working-directory: spm/${{ matrix.name }}
env:
PKG_CONFIG_PATH: ${{ github.workspace }}/spm/${{ matrix.name }}/vendor/pkgconfig
ARGS: ${{ matrix.swiftpm }}
run: |
status=0
while read -r manifest; do
package=$(dirname "$manifest")
echo "::group::$package"
if ! (cd "$package" && swift package dump-package |
jq -e '[.targets[] | select(.type != "plugin")] | length > 0' > /dev/null); then
echo "only plugins here, nothing to build"
elif ! (cd "$package" && swift build $ARGS); then
echo "::error::swift build failed for $package"
status=1
elif [ -d "$package/Tests" ] && ! (cd "$package" && swift test $ARGS); then
echo "::error::swift test failed for $package"
status=1
fi
echo "::endgroup::"
done < <(find . -name Package.swift -not -path "*/App/*" -not -path "*/.build/*" | sort)
exit $status

# A package that wraps a system library is found through `pkg-config`,
# and a fixture that ships its own `.pc` is only found when that file's
# directory is on the path.
#
# A note is the generator saying the build differs from what the package
# asked for — a plugin that did not run, a program whose resources
# nothing can bundle, a library pkg-config has never heard of. Every one
# of those builds and tests green and is wrong at run time, so no fixture
# may produce one silently: a lane expecting a note names it in `notes`,
# and anything else said is a failure.
- name: Bazel Generation
env:
PKG_CONFIG_PATH: ${{ github.workspace }}/spm/${{ matrix.name }}/vendor/pkgconfig
NOTES: ${{ matrix.notes }}
run: |
chmod +x bazelize
./bazelize --project "spm/${{ matrix.name }}" --output "spm/${{ matrix.name }}/App"

# A build tool plugin is a program: Bazel builds it and bazelize runs it,
# which is what puts the sources it generates where the rules glob for
# them. Only the packages that have one need the step.
./bazelize --input "spm/${{ matrix.name }}" --output "spm/${{ matrix.name }}/App" | tee bazelize.log

if [ -n "$NOTES" ]; then
if ! grep -qE "$NOTES" bazelize.log; then
echo "::error::the note this fixture is here for was not said: $NOTES"
exit 1
fi
elif [ -s bazelize.log ]; then
echo "::error::the generator said something no fixture expects"
exit 1
fi

# Bazel builds the plugin host, plugins and tools, then runs them from
# runfiles. No separately installed bazelize executable is involved.
# Only the packages that have a build tool plugin need the step.
- name: Run Plugins
if: matrix.plugins
working-directory: spm/${{ matrix.name }}/App
run: PATH="$GITHUB_WORKSPACE:$PATH" bazel run //:plugins
run: bazel run //:plugins

# The workspace's own commands, which answer whatever the package is:
# a package with no trait, no configuration and no localization answers
# that.
- name: List Workspace
working-directory: spm/${{ matrix.name }}/App
run: |
PATH="$GITHUB_WORKSPACE:$PATH" bazel list config
PATH="$GITHUB_WORKSPACE:$PATH" bazel list trait
PATH="$GITHUB_WORKSPACE:$PATH" bazel list language

# A build that names a localization bundles that one and `Base`, and
# nothing else. What is in the bundle is the only proof of that.
- name: Filter Localizations
if: matrix.name == 'TargetResource'
working-directory: spm/TargetResource/App
run: |
bazel build //Packages/TargetResource:TargetResourceTests --config=lang.en
bundle=$(find -L bazel-bin/Packages/TargetResource -name TargetResourceTests.zip | head -1)
unzip -Z1 "$bundle" | grep -q "en.lproj"
unzip -Z1 "$bundle" | grep -q "Base.lproj"
! unzip -Z1 "$bundle" | grep -q "ja.lproj"
! unzip -Z1 "$bundle" | grep -q "de.lproj"

- name: Test Package
working-directory: spm/${{ matrix.name }}/App
run: bazel test //...

# A program is not a test: the rules that build one carry `manual`, so
# `//...` never builds it. Running it is what says it links and works.
- name: Run Package Programs
if: matrix.programs
working-directory: spm/${{ matrix.name }}/App
env:
PROGRAMS: ${{ matrix.programs }}
run: |
while read -r program; do
[ -n "$program" ] || continue
bazel run $program
done <<< "$PROGRAMS"

# Both ways round, and not only that both pass: a trait is a compilation
# condition named after itself, so the flag reaching swiftc is the thing
# to see.
- name: Test Package With Trait
if: matrix.config
working-directory: spm/${{ matrix.name }}/App
run: |
bazel test //... --config=${{ matrix.config }}
trait="${{ matrix.config }}"
! bazel aquery 'mnemonic("SwiftCompile", //...)' | grep -q -- "-D${trait#*.}"
bazel aquery --config=${{ matrix.config }} 'mnemonic("SwiftCompile", //...)' | grep -q -- "-D${trait#*.}"

# Select each declared trait on its own, through the `--config` the
# workspace generated for it. That is what `swift test --traits <trait>`
# does: the selection replaces the package's defaults, and carries
# whatever that trait enables.
- name: Test Each Package Trait
if: matrix.traits
working-directory: spm/${{ matrix.name }}/App
env:
PACKAGE: ${{ matrix.name }}
TRAITS: ${{ matrix.traits }}
run: |
IFS=',' read -ra traits <<< "$TRAITS"
for trait in "${traits[@]}"; do
bazel test //... "--config=${PACKAGE}.${trait}"
done

# What each selection resolves to, exactly: the closure of the trait
# asked for, and nothing of the defaults it replaced.
- name: Probe Trait Selections
if: matrix.name == 'TraitGraph'
working-directory: spm/TraitGraph/App
run: |
bazel run //Packages/TraitGraph:TraitProbe -- Leaf Middle Top
bazel run //Packages/TraitGraph:TraitProbe --config=TraitGraph.Middle -- Leaf Middle
bazel run //Packages/TraitGraph:TraitProbe --config=TraitGraph.Alternative -- Alternative
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Bazelize

A cli tool turn your xcode project or Swift package to bazel.
Bazelize generates Bazel workspaces from Xcode projects and Swift packages.

---

Expand All @@ -13,13 +13,13 @@ mint install XCodeBazelize/Bazelize
## Usage

```sh
bazelize --project YOUR.xcodeproj
bazelize --input YOUR.xcodeproj --output App
```

Or a Swift package — the `Package.swift`, or the directory holding one:

```sh
bazelize --project path/to/Package.swift
bazelize --input path/to/Package.swift --output App
```

---
Expand Down
2 changes: 1 addition & 1 deletion Sources/BazelRules/Rules+Apple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,7 @@ extension Rules.Apple.General {
deps: [Starlark.Label],
avoid_deps: Starlark.Value? = nil,
data: Starlark.Value? = nil,
linkopts: [String]? = nil,
linkopts: Starlark.Value? = nil,
minimum_os_version: String? = nil,
platform_type: String? = nil,
sdk_dylibs: [String]? = nil,
Expand Down
16 changes: 16 additions & 0 deletions Sources/BazelRules/Rules+Builtin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ extension Rules {
extension Rules.Builtin.Call {
public static func config_setting(
name: String,
values: [String: String]? = nil,
flag_values: [String: String]? = nil,
visibility: Starlark.Statement.Argument.Visibility? = nil)
-> Starlark.Statement.Call
{
.init("config_setting") {
"name" => name
if let values {
"values" => values
}
if let flag_values {
"flag_values" => flag_values
}
Expand All @@ -27,6 +31,18 @@ extension Rules.Builtin.Call {
}
}

/// The `module(...)` directive every `MODULE.bazel` opens with.
public static func module(
name: String,
version: String)
-> Starlark.Statement.Call
{
.init("module") {
"name" => name
"version" => version
}
}

public static func bazel_dep(
name: String,
version: String,
Expand Down
6 changes: 4 additions & 2 deletions Sources/BazelRules/Rules+Cc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ extension Rules.Cc {
srcs: Starlark.Value? = nil,
hdrs: Starlark.Value? = nil,
deps: Starlark.Value? = nil,
copts: [String]? = nil,
copts: Starlark.Value? = nil,
defines: [String]? = nil,
includes: [String]? = nil,
linkopts: [String]? = nil,
linkopts: Starlark.Value? = nil,
tags: [String]? = nil,
textual_hdrs: Starlark.Value? = nil,
visibility: Starlark.Statement.Argument.Visibility? = nil)
Expand All @@ -63,6 +64,7 @@ extension Rules.Cc {
if let hdrs { "hdrs" => hdrs }
if let deps { "deps" => deps }
if let copts { "copts" => copts }
if let defines { "defines" => defines }
if let includes { "includes" => includes }
if let linkopts { "linkopts" => linkopts }
if let tags { "tags" => tags }
Expand Down
Loading
Loading