Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resources to package #138

Closed
wants to merge 15 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ fn main() {
)]),
out_dir: PathBuf::from("swift-package-rust-library-fixture/MySwiftPackage"),
package_name: "MySwiftPackage".to_string(),
resources: vec![],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some resources?

  • A file resource-test.txt
  • A directory with 2 files `directory-with-files/{a,b}.txt
  • A directory that contains a directory that contains a file nested/directory/test.txt

Then right after this create_package call we can call a fn assert_resources_bundled

Which checks the "swift-package-rust-library-fixture/MySwiftPackage" Sources dir to make sure that the resources are in there.


Also, if there's a way to check that the resources are accessible on the Swift side, that would be great. Could check in here

final class swift_package_test_packageTests: XCTestCase {
func testPackageRun() throws {
XCTAssertEqual("Hello, From Rust!", hello_rust().toString())
}
func testInstantiateSharedStruct() throws {
XCTAssertEqual(SomeStruct(field: 1).field, 1);
}
func testInstantiateSharedStructUnnamed() throws {
XCTAssertEqual(UnnamedStruct(_0: 1)._0, 1);
}
}

});
}
16 changes: 16 additions & 0 deletions book/src/building/swift-packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,19 @@ cd SwiftProject
swift run
# You should see "Hello from Rust!" in your terminal.
```

## Adding resources

You can add resources to your Swift Package by specifying them when running the `swift-bridge-cli`
with the `--resource` flag. Use colon-separated source and destination paths.
Examples:

- `--resource=source:destination`
- `--resource=some_folder/some_file.txt:to_folder/some_file.txt`
- `--resource=source_folder:destination_folder`.

These resources will be accessible from your Swift Package's `Bundle` as a property.

```swift
Bundle.<camel cased cargo package name>
```
5 changes: 3 additions & 2 deletions crates/swift-bridge-build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license = "Apache-2.0/MIT"

[dependencies]
proc-macro2 = "1"
swift-bridge-ir = {version = "0.1.48", path = "../swift-bridge-ir"}
syn = {version = "1"}
swift-bridge-ir = { version = "0.1.45", path = "../swift-bridge-ir" }
syn = { version = "1" }
tempfile = "3.3"
copy_dir = "0.1"
1 change: 1 addition & 0 deletions crates/swift-bridge-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

mod package;
use crate::generate_core::write_core_swift_and_c;
pub use crate::package::CopyBundleResourceDesc;
pub use package::*;
use std::path::Path;
use swift_bridge_ir::{CodegenConfig, SwiftBridgeModule};
Expand Down
145 changes: 135 additions & 10 deletions crates/swift-bridge-build/src/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pub struct CreatePackageConfig {
pub out_dir: PathBuf,
/// The name for the Swift package
pub package_name: String,
/// Additional resources to copy into the package, first PathBuf is source and second is destination
pub resources: Vec<CopyBundleResourceDesc>,
}

impl CreatePackageConfig {
Expand All @@ -27,12 +29,14 @@ impl CreatePackageConfig {
paths: HashMap<ApplePlatform, PathBuf>,
out_dir: PathBuf,
package_name: String,
resources: Vec<CopyBundleResourceDesc>,
) -> Self {
Self {
bridge_dir,
paths,
out_dir,
package_name,
resources,
}
}
}
Expand Down Expand Up @@ -142,7 +146,7 @@ fn gen_xcframework(output_dir: &Path, config: &CreatePackageConfig) {
bridge_dir.join("SwiftBridgeCore.h"),
&include_dir.join("SwiftBridgeCore.h"),
)
.expect("Couldn't copy SwiftBirdgeCore header file");
.expect("Couldn't copy SwiftBridgeCore header file");
let bridge_project_dir = fs::read_dir(&bridge_dir)
.expect("Couldn't read generated directory")
.find_map(|file| {
Expand Down Expand Up @@ -299,15 +303,43 @@ fn gen_package(output_dir: &Path, config: &CreatePackageConfig) {
}
})
.expect("Couldn't find project's bridging swift file");
fs::write(
sources_dir.join(&bridge_project_swift_dir.file_name().unwrap()),

let bridge_swift = fs::read_to_string(&bridge_project_swift_dir)
.expect("Couldn't read project's bridging swift file");

let bridge_content = if config.resources.is_empty() {
format!("import RustXcframework\n{bridge_swift}")
} else {
// add a Bundle extension with a convenience accessor for the resources making
// them accessible from Swift with `Bundle.<package_name>`
format!(
"import RustXcframework\n{}",
fs::read_to_string(&bridge_project_swift_dir)
.expect("Couldn't read project's bridging swift file")
),
)
.expect("Couldn't copy project's bridging swift file to the package");
r#"import RustXcframework
import Foundation

{bridge_swift}

extension Bundle {{
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment in code

public static var {}: Bundle = .module
}}
"#,
first_lowercased(&config.package_name)
)
};

let bridge_path = sources_dir.join(&bridge_project_swift_dir.file_name().unwrap());

fs::write(bridge_path, bridge_content)
.expect("Couldn't copy project's bridging swift file to the package");

let resource_entries = config
.resources
.iter()
.map(|r| {
let name = r.copy(&sources_dir);
format!(" .copy(\"{name}\")")
})
.collect::<Vec<String>>()
.join("\n");

// Generate Package.swift
let package_name = &config.package_name;
Expand All @@ -329,7 +361,11 @@ let package = Package(
),
.target(
name: "{package_name}",
dependencies: ["RustXcframework"])
dependencies: ["RustXcframework"],
resources: [
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move the package string generation out into its own function

let package_swift = format!(
r#"// swift-tools-version:5.5.0
import PackageDescription
let package = Package(
name: "{package_name}",
products: [
.library(
name: "{package_name}",
targets: ["{package_name}"]),
],
dependencies: [],
targets: [
.binaryTarget(
name: "RustXcframework",
path: "RustXcframework.xcframework"
),
.target(
name: "{package_name}",
dependencies: ["RustXcframework"])
]
)
"#
);

Then add a test to the bottom of this file that generates the package description for the CreatePackageConfig that has resources and asserts on the contents of the generated string?

My main concern here is that right now the writing to disk / process spawning stuff is tightly coupled to things that it doesn't need to be (I know this was here before you, so not saying that you did that!).

So I'm asking to add a test for this resources thing so that we at least begin take a small step in the right direction and make it easier to make our Swift package creation more powerful over time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did make an effort to follow your (current) style of programming, imitating how things are done. I think it is better if you, as the owner of this project, set the new precedence instead of me as I will probably not do it as you like it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually all of the package stuff was contributed by a contributor.

However, makes sense to me!

{resource_entries}
]
)
]
)
"#
Expand All @@ -338,3 +374,92 @@ let package = Package(
fs::write(output_dir.join("Package.swift"), package_swift)
.expect("Couldn't write Package.swift file");
}

/// make first char lowercase
fn first_lowercased(name: &str) -> String {
let mut c = name.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_lowercase().collect::<String>() + c.as_str(),
}
}

/// Describes a bundle resource to copy from some source path to some destination.
///
/// The source is a path to an existing file or directory that you wish to bundle.
///
/// The destination is a relative path that the resource will get copied to within the
/// generated Swift package's `Sources/YourLibraryName` directory.
///
/// # Examples
///
/// ```
/// # use swift_bridge_build::CopyBundleResourceDesc;
///
/// // If you are creating a Swift Package called "MyLibrary", this descriptor will
/// // lead to the package generator copying the `/path/to/images` directory to the
/// // the Swift package's `Sources/MyLibrary/assets/icons` directory.
/// let resource = CopyBundleResourceDesc::new("/path/to/images", "assets/icons");
/// ```
pub struct CopyBundleResourceDesc {
source: PathBuf,
destination: PathBuf,
}

impl CopyBundleResourceDesc {
/// Parse a colon-separated pair of paths into a [`CopyBundleResourceDesc`].
///
/// Examples: `source:destination`, `some_folder/some_file.txt:to_folder/some_file.txt`
/// or `source_folder:destination_folder`.
///
/// # Panics
/// Panics if there is no colon in the pair.
pub fn from(pair: String) -> Self {
let mut split = pair.split(':');
let from = PathBuf::from(split.next().unwrap());
let to = PathBuf::from(
split
.next()
.expect(&format!("Invalid resource pair: {pair}")),
);
if !from.exists() {
panic!("Resource file does not exist: {from:?}");
}
if to.is_absolute() {
panic!("Resource destination must be relative: {to:?}");
}
Self {
source: from,
destination: to,
}
}
/// See [`CopyBundleResourceDesc`] for documentation.
///
/// # Panics
/// Panics if the destination is an absolute path.
pub fn new(source: impl Into<PathBuf>, destination: impl Into<PathBuf>) -> Self {
let destination = destination.into();
assert!(destination.is_relative());
Self {
source: source.into(),
destination,
}
}

fn copy(&self, sources_dir: &Path) -> String {
let to = sources_dir.join(&self.destination);

if self.source.is_dir() {
copy_dir::copy_dir(&self.source, &to).expect(&format!(
"Could not copy resource directory {:?} to {to:?}",
self.source
));
} else {
if let Some(parent) = to.parent() {
fs::create_dir_all(parent).expect("Couldn't create directory for resource");
}
fs::copy(&self.source, to).expect("Couldn't copy resource");
}
self.destination.display().to_string()
}
}
14 changes: 14 additions & 0 deletions crates/swift-bridge-cli/src/clap_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,18 @@ fn create_package_command() -> Command<'static> {
.required(true)
.help("The name for the Swift Package"),
)
.arg(
Arg::new("resource")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you land on the : separator approach? Were there any other alternatives considered?

Also, can we leave a TODO comment that whenever clap supports array arguments we should switch to switch to using [PathBuf; 2] instead clap-rs/clap#1682

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, it's not a great solution to use :, but I didn't really find any easy and good solution so I went for the easy one :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the TODO

.long("resource")
.takes_value(true)
// TODO whenever clap supports array arguments we should switch to switch to using [PathBuf; 2] instead.
// See: https://github.com/clap-rs/clap/issues/1682
.value_name("SRC_PATH:DEST_PATH")
.multiple_values(true)
.help(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we update the book example to include bundling a resource?

```rust
use std::path::PathBuf;
use std::collections::HashMap;
use swift_bridge_build::{CreatePackageConfig, ApplePlatform};
fn main() {
swift_bridge_build::create_package(CreatePackageConfig {
bridge_dir: PathBuf::from("./generated"),
paths: HashMap::from([
(ApplePlatform::IOS, "target/aarch64-apple-ios/debug/libmy_rust_lib.a".into()),
(ApplePlatform::Simulator, "target/universal-ios/debug/libmy_rust_lib.a".into()),
(ApplePlatform::MacOS, "target/universal-macos/debug/libmy_rust_lib.a".into()),
]),
out_dir: PathBuf::from("MySwiftPackage"),
package_name: PathBuf::from("MySwiftPackage")
});
}
```
#### CLI
You can use the `swift-bridge` CLI's `create-package` command in order to create a Swift Package.
First, install the CLI.
```bash
cargo install -f swift-bridge-cli
swift-bridge-cli --help
```
Then, run the following to package up your generated bridges and your Rust libraries into a Swift Package.
```bash
swift-bridge-cli create-package \
--bridges-dir ./generated \
--out-dir MySwiftPackage \
--ios target/aarch64-apple-ios/debug/libmy_rust_lib.a \
--simulator target/universal-ios/debug/libmy_rust_lib.a \
--macos target/universal-macos/debug/libmy_rust_lib.a \
--name MySwiftPackage
```

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it to the end as it is not mandatory to add resources.

"A resource to copy to the package. \
Use colon-separated source and destination paths. \
Ex: --resource=./folder/myresource.txt:packagefolder/myresource.txt",
),
)
}
18 changes: 17 additions & 1 deletion crates/swift-bridge-cli/src/clap_exec.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use clap::ArgMatches;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use swift_bridge_build::{create_package, ApplePlatform, CreatePackageConfig};
use swift_bridge_build::{
create_package, ApplePlatform, CopyBundleResourceDesc, CreatePackageConfig,
};

/// Executes the correct function depending on the cli input
pub fn handle_matches(matches: ArgMatches) {
Expand All @@ -18,12 +20,14 @@ fn handle_create_package(matches: &ArgMatches) {
let bridges_dir = matches.value_of("bridges-dir").unwrap(); // required
let out_dir = matches.value_of("out-dir").map(|p| Path::new(p)).unwrap(); // required
let name = matches.value_of("name").unwrap(); // required
let resources = parse_copybundleresourcedesc_args(matches);

let mut config = CreatePackageConfig {
bridge_dir: PathBuf::from(bridges_dir),
paths: HashMap::new(),
out_dir: out_dir.to_path_buf(),
package_name: name.to_string(),
resources,
};

for platform in ApplePlatform::ALL {
Expand All @@ -34,3 +38,15 @@ fn handle_create_package(matches: &ArgMatches) {

create_package(config);
}

fn parse_copybundleresourcedesc_args(matches: &ArgMatches) -> Vec<CopyBundleResourceDesc> {
let pairs: Vec<String> = matches
.get_many("resource")
.map(|v| v.cloned().collect())
.unwrap_or_default();

pairs
.into_iter()
.map(CopyBundleResourceDesc::from)
.collect()
}