Skip to content
Open
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
56 changes: 56 additions & 0 deletions docs/bazel.md
Original file line number Diff line number Diff line change
Expand Up @@ -963,3 +963,59 @@ xcodeproj_extra_files(
| <a id="xcodeproj_extra_files-files"></a>files | The list of extra files to surface in the Xcode navigator. | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | `[]` |


<a id="xcodeproj_synchronized_folders"></a>

## xcodeproj_synchronized_folders

<pre>
load("@rules_xcodeproj//xcodeproj/internal/docs:aspect_hints.bzl", "xcodeproj_synchronized_folders")

xcodeproj_synchronized_folders(<a href="#xcodeproj_synchronized_folders-name">name</a>, <a href="#xcodeproj_synchronized_folders-folders">folders</a>)
</pre>

This rule is used to show a target's source folders in Xcode as synchronized
folders (Xcode 16's buildable folders), whose contents Xcode keeps up to date on
its own: files added to such a folder appear in the target without regenerating
the project. The provider created by this rule should be attached to the
related target via an aspect hint.

The folders are relative to the package of the target that the hint is attached
to, so one hint can be shared by targets in different packages that use the
same layout. The target's sources inside the folders are shown by the folders
instead of individually.

Bazel still only compiles the target's `srcs`, so they should be every source
file in the folders, typically with a `glob`; otherwise a file added in Xcode
appears in the target but isn't built.

**EXAMPLE**

```starlark
load(
"@rules_xcodeproj//xcodeproj:xcodeproj_synchronized_folders.bzl",
"xcodeproj_synchronized_folders",
)

swift_library(
...
srcs = glob(["Sources/**/*.swift"]),
aspect_hints = [":sources_folder"],
...
)

# Show `Sources` as a synchronized folder of the Swift library in Xcode
xcodeproj_synchronized_folders(
name = "sources_folder",
folders = ["Sources"],
)
```

**ATTRIBUTES**


| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| <a id="xcodeproj_synchronized_folders-name"></a>name | A unique name for this target. | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required | |
| <a id="xcodeproj_synchronized_folders-folders"></a>folders | The folders to show as synchronized folders, relative to the package of the target that the hint is attached to. | List of strings | required | |


2 changes: 2 additions & 0 deletions test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ test_suite(
"//test/internal/target_id",
"//test/internal/targets",
"//test/internal/xcodeproj_extra_files",
"//test/internal/xcodeproj_synchronized_folders",
"//test/internal/xcschemes",
],
)
Expand All @@ -30,6 +31,7 @@ bzl_library(
"//test/internal/target_id:bzls",
"//test/internal/targets:bzls",
"//test/internal/xcodeproj_extra_files:bzls",
"//test/internal/xcodeproj_synchronized_folders:bzls",
"//test/internal/xcschemes:bzls",
],
)
Expand Down
21 changes: 21 additions & 0 deletions test/internal/pbxproj_partials/write_files_and_groups_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ _FILE_PATHS_FILE = mock_actions.mock_file(
_GENERATED_FILE_PATHS_FILE = mock_actions.mock_file(
"a_generator_name_pbxproj_partials/generated_file_paths_file",
)
_SYNCHRONIZED_FOLDERS_FILE = mock_actions.mock_file(
"a_generator_name_pbxproj_partials/synchronized_folders_file",
)

def _write_files_and_groups_test_impl(ctx):
env = unittest.begin(ctx)
Expand All @@ -35,12 +38,14 @@ def _write_files_and_groups_test_impl(ctx):
_RESOLVED_REPOSITORIES_FILE_DECLARED_FILE: None,
_FILE_PATHS_FILE: None,
_GENERATED_FILE_PATHS_FILE: None,
_SYNCHRONIZED_FOLDERS_FILE: None,
}
expected_inputs = [
_FILE_PATHS_FILE,
_GENERATED_FILE_PATHS_FILE,
ctx.attr.execution_root_file,
ctx.attr.selected_model_versions_file,
_SYNCHRONIZED_FOLDERS_FILE,
] + ctx.attr.buildfile_subidentifiers_files
expected_outputs = [
_FILES_AND_GROUPS_DECLARED_FILE,
Expand Down Expand Up @@ -91,6 +96,7 @@ def _write_files_and_groups_test_impl(ctx):
install_path = ctx.attr.install_path,
project_options = ctx.attr.project_options,
selected_model_versions_file = ctx.attr.selected_model_versions_file,
synchronized_folders = depset(ctx.attr.synchronized_folders),
tool = None,
workspace_directory = ctx.attr.workspace_directory,
)
Expand Down Expand Up @@ -190,6 +196,7 @@ write_files_and_groups_test = unittest.make(
"install_path": attr.string(mandatory = True),
"project_options": attr.string_dict(mandatory = True),
"selected_model_versions_file": attr.string(mandatory = True),
"synchronized_folders": attr.string_list(),
"workspace_directory": attr.string(mandatory = True),

# Expected
Expand Down Expand Up @@ -223,6 +230,7 @@ def write_files_and_groups_test_suite(name):
install_path,
project_options,
selected_model_versions_file,
synchronized_folders = [],
workspace_directory,

# Expected
Expand All @@ -244,6 +252,7 @@ def write_files_and_groups_test_suite(name):
install_path = install_path,
project_options = project_options,
selected_model_versions_file = selected_model_versions_file,
synchronized_folders = synchronized_folders,
workspace_directory = workspace_directory,

# Expected
Expand Down Expand Up @@ -300,6 +309,9 @@ def write_files_and_groups_test_suite(name):
_GENERATED_FILE_PATHS_FILE.path,
# developmentRegion
"en",
# synchronizedFoldersFile
"--synchronized-folders-file",
_SYNCHRONIZED_FOLDERS_FILE.path,
# useBaseInternationalization
"--use-base-internationalization",
# buildFileSubIdentifiersFiles
Expand All @@ -310,6 +322,7 @@ def write_files_and_groups_test_suite(name):
expected_writes = {
_FILE_PATHS_FILE: "\n",
_GENERATED_FILE_PATHS_FILE: "\n",
_SYNCHRONIZED_FOLDERS_FILE: "\n",
},
)

Expand All @@ -325,6 +338,7 @@ def write_files_and_groups_test_suite(name):
],
colorize = True,
compile_stub_needed = True,
synchronized_folders = ["a/path/to/Sources"],
execution_root_file = "an/execution/root/file",
files = [
"a/path/to/a/file",
Expand Down Expand Up @@ -384,6 +398,9 @@ def write_files_and_groups_test_suite(name):
_GENERATED_FILE_PATHS_FILE.path,
# developmentRegion
"enGB",
# synchronizedFoldersFile
"--synchronized-folders-file",
_SYNCHRONIZED_FOLDERS_FILE.path,
# useBaseInternationalization
"--use-base-internationalization",
# compileStubNeeded
Expand All @@ -401,6 +418,10 @@ a/path/to/a/file
another/path/to/another/file
a/path/to/a/file_path.bundle
another/path/to/another/file_path.framework
a/path/to/Sources
""",
_SYNCHRONIZED_FOLDERS_FILE: """\
a/path/to/Sources
""",
_GENERATED_FILE_PATHS_FILE: """\
file
Expand Down
15 changes: 15 additions & 0 deletions test/internal/xcodeproj_synchronized_folders/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load(":xcodeproj_synchronized_folders_tests.bzl", "xcodeproj_synchronized_folders_test_suite")

xcodeproj_synchronized_folders_test_suite(name = "xcodeproj_synchronized_folders_test")

test_suite(name = "xcodeproj_synchronized_folders")

bzl_library(
name = "bzls",
srcs = glob(
["*.bzl"],
exclude = ["utils.bzl"],
),
visibility = ["//test:__pkg__"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Tests for the `xcodeproj_synchronized_folders` module."""

load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
load(
"//xcodeproj:xcodeproj_synchronized_folders.bzl",
"xcodeproj_synchronized_folders",
)

# buildifier: disable=bzl-visibility
load(
"//xcodeproj/internal:providers.bzl",
"XcodeProjSynchronizedFoldersHintInfo",
)

def _provider_contents_test_impl(ctx):
env = analysistest.begin(ctx)

target_under_test = analysistest.target_under_test(env)

asserts.equals(
env,
("Sources", "Tests/Unit"),
target_under_test[XcodeProjSynchronizedFoldersHintInfo].folders,
)

return analysistest.end(env)

provider_contents_test = analysistest.make(_provider_contents_test_impl)

def _invalid_folder_test_impl(ctx):
env = analysistest.begin(ctx)
asserts.expect_failure(env, "must be a path relative to a package")
return analysistest.end(env)

invalid_folder_test = analysistest.make(
_invalid_folder_test_impl,
expect_failure = True,
)

def _test_provider_contents():
xcodeproj_synchronized_folders(
name = "xcodeproj_synchronized_folders_subject",
folders = ["Sources", "Tests/Unit"],
tags = ["manual"],
)

provider_contents_test(
name = "provider_contents_test",
target_under_test = ":xcodeproj_synchronized_folders_subject",
)

def _test_invalid_folder():
xcodeproj_synchronized_folders(
name = "xcodeproj_synchronized_folders_invalid_subject",
folders = ["../Sources"],
tags = ["manual"],
)

invalid_folder_test(
name = "invalid_folder_test",
target_under_test = ":xcodeproj_synchronized_folders_invalid_subject",
)

def xcodeproj_synchronized_folders_test_suite(name):
_test_provider_contents()
_test_invalid_folder()

native.test_suite(
name = name,
tests = [
":invalid_folder_test",
":provider_contents_test",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,31 @@ extension ElementCreator {
struct CreateFileElement {
private let createAttributes: CreateAttributes
private let createIdentifier: CreateIdentifier
private let synchronizedFolders: Set<BazelPath>

private let callable: Callable

/// - Parameters:
/// - synchronizedFolders: The paths of folders to create as
/// `PBXFileSystemSynchronizedRootGroup`s.
/// - callable: The function that will be called in
/// `callAsFunction()`.
init(
createAttributes: CreateAttributes,
createIdentifier: CreateIdentifier,
synchronizedFolders: Set<BazelPath> = [],
callable: @escaping Callable
) {
self.createAttributes = createAttributes
self.createIdentifier = createIdentifier
self.synchronizedFolders = synchronizedFolders

self.callable = callable
}

/// Creates a `PBXFileReference` element.
/// Creates a `PBXFileReference` element, or a
/// `PBXFileSystemSynchronizedRootGroup` element for a synchronized
/// folder.
func callAsFunction(
name: String,
ext: String?,
Expand All @@ -31,6 +38,15 @@ extension ElementCreator {
element: Element,
resolvedRepository: ResolvedRepository?
) {
if synchronizedFolders.contains(bazelPath) {
return Self.synchronizedRootGroup(
name: name,
bazelPath: bazelPath,
bazelPathType: bazelPathType,
createAttributes: createAttributes
)
}

return callable(
/*name:*/ name,
/*ext:*/ ext,
Expand Down Expand Up @@ -118,6 +134,59 @@ sourceTree = \(attributes.elementAttributes.sourceTree.rawValue); }
)
}

/// Creates a `PBXFileSystemSynchronizedRootGroup` element, which Xcode
/// fills with the contents of the folder at `bazelPath`.
static func synchronizedRootGroup(
name: String,
bazelPath: BazelPath,
bazelPathType: BazelPathType,
createAttributes: ElementCreator.CreateAttributes
) -> (
element: Element,
resolvedRepository: ResolvedRepository?
) {
let attributes = createAttributes(
name: name,
bazelPath: bazelPath,
bazelPathType: bazelPathType,
isGroup: true
)

let nameAttribute: String
if let name = attributes.elementAttributes.name {
nameAttribute = "name = \(name.pbxProjEscaped); "
} else {
nameAttribute = ""
}
// Xcode adds the empty `explicitFileTypes` and `explicitFolders` when
// the folder's contents change, rewriting the whole project, unless
// they are already present.
let content = """
{isa = PBXFileSystemSynchronizedRootGroup; \
explicitFileTypes = {}; explicitFolders = (); \
\(nameAttribute)\
path = \(attributes.elementAttributes.path.pbxProjEscaped); \
sourceTree = \(attributes.elementAttributes.sourceTree.rawValue); }
"""

return (
element: .init(
name: name,
object: .init(
identifier: Identifiers.FilesAndGroups
.synchronizedRootGroup(
bazelPath.path,
name: attributes.elementAttributes.name ??
attributes.elementAttributes.path
),
content: content
),
sortOrder: .groupLike
),
resolvedRepository: attributes.resolvedRepository
)
}

private static func calculateFileTypeType(
basename: String,
extension: String?
Expand Down
Loading