Skip to content

Commit

Permalink
cli: Support workspaces with --package flag
Browse files Browse the repository at this point in the history
Previously `powerpack` only worked with the root package of a workspace.
You can now have multiple packages in a workspace and use the
`--package` flag to specify which package to build, link or package. The
`workflow/` directory containing the package information must be in the
same directory as the manifest file for the particular package.
  • Loading branch information
rossmacarthur committed Nov 26, 2023
1 parent beb2820 commit 32b8bf1
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 54 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[workspace]
members = ["crates/*", "examples/*"]

[package]
name = "powerpack"
version = "0.4.2"
Expand All @@ -23,6 +26,3 @@ goldie = "0.2.0"
default = ["env"]
detach = ["powerpack-detach"]
env = ["powerpack-env"]

[workspace]
members = ["crates/*"]
33 changes: 25 additions & 8 deletions crates/cli/src/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub enum Mode {
#[derive(Debug)]
pub struct Metadata {
pub workspace_dir: PathBuf,
pub manifest_dir: PathBuf,
pub target_dir: PathBuf,
pub package_name: String,
pub binary_names: Vec<String>,
Expand Down Expand Up @@ -72,8 +73,16 @@ where
}

/// Run a `cargo build` command.
pub fn build(mode: Mode, bins: &[String], target: Option<&str>) -> Result<()> {
pub fn build(
mode: Mode,
package: Option<&str>,
bins: &[String],
target: Option<&str>,
) -> Result<()> {
let mut cmd = Cargo::new("build");
if let Some(package) = package {
cmd.arg("--package").arg(package);
}
if let Mode::Release = mode {
cmd.arg("--release");
}
Expand All @@ -89,7 +98,7 @@ pub fn build(mode: Mode, bins: &[String], target: Option<&str>) -> Result<()> {
}

/// Run a `cargo metadata` command.
pub fn metadata() -> Result<Metadata> {
pub fn metadata(package: Option<&str>) -> Result<Metadata> {
let metadata::Metadata {
packages,
workspace_root,
Expand All @@ -98,12 +107,19 @@ pub fn metadata() -> Result<Metadata> {
..
} = metadata::MetadataCommand::new().exec()?;

let root = move || {
let root = resolve.as_ref()?.root.as_ref()?;
packages.into_iter().find(|pkg| &pkg.id == root)
let pkg = match package {
Some(n) => packages
.into_iter()
.find(|pkg| pkg.name == n)
.with_context(|| format!("package not found: `{}`", n))?,
None => (move || {
let root = resolve.as_ref()?.root.as_ref()?;
packages.into_iter().find(|pkg| &pkg.id == root)
})()
.context("no root package")?,
};
let package = root().context("no root package")?;
let binary_names = package

let binary_names = pkg
.targets
.into_iter()
.filter(|target| target.kind.iter().any(|kind| kind == "bin"))
Expand All @@ -112,8 +128,9 @@ pub fn metadata() -> Result<Metadata> {

Ok(Metadata {
workspace_dir: workspace_root.into(),
manifest_dir: pkg.manifest_path.parent().unwrap().into(),
target_dir: target_directory.into(),
package_name: package.name,
package_name: pkg.name,
binary_names,
})
}
Expand Down
115 changes: 72 additions & 43 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod cargo;

use std::env;
use std::ffi::OsString;
use std::fmt;
use std::fs;
use std::io::prelude::*;
use std::os::unix::fs::symlink;
Expand All @@ -21,6 +22,14 @@ fn print(header: &str, message: impl AsRef<str>) {
}
}

fn print_warning(header: &str, message: impl AsRef<str>) {
if atty::is(atty::Stream::Stdout) {
eprintln!("{:>12} {}", header.bold().yellow(), message.as_ref());
} else {
eprintln!("{:>12} {}", header, message.as_ref());
}
}

fn prompt_for_workflow_info(doc: &toml::Document) -> Result<alfred::WorkflowInfo> {
let package_name = doc["package"]["name"].as_str().context("expected string")?;
println!("Please enter the workflow details:");
Expand Down Expand Up @@ -73,49 +82,52 @@ fn init(manifest_dir: &Path, name: Option<OsString>) -> Result<()> {
}

/// Build the workflow.
fn build(bins: Vec<String>, release: bool, target: Option<String>) -> Result<()> {
fn build(
package: Option<&str>,
bins: Vec<String>,
release: bool,
target: Option<&str>,
) -> Result<()> {
let mode = if release {
cargo::Mode::Release
} else {
cargo::Mode::Debug
};
cargo::build(mode, &bins, target.as_deref())?;
cargo::build(mode, package, &bins, target)?;

let metadata = cargo::metadata()?;
let workflow_dir = metadata.workspace_dir.join("workflow");
let metadata = cargo::metadata(package)?;
let workflow_dir = metadata.manifest_dir.join("workflow");
fs::create_dir_all(&workflow_dir)?;

let src_dir = match target {
Some(target) => metadata.target_dir.join(target).join(mode.dir()),
None => metadata.target_dir.join(mode.dir()),
};

for binary_name in &metadata.binary_names {
if !bins.is_empty() && !bins.contains(binary_name) {
continue;
}
let binary_names: Vec<_> = metadata
.binary_names
.iter()
.filter(|binary_name| bins.is_empty() || bins.contains(binary_name))
.collect();

if binary_names.is_empty() {
print_warning(
"Warning",
format!("package `{}` has no binaries", metadata.package_name),
);
return Ok(());
}

for binary_name in &binary_names {
let src = src_dir.join(binary_name);
let dst = workflow_dir.join(binary_name);
let removed = fs::remove_file(&dst).is_ok();
fs::copy(src, &dst)?;

if removed {
print(
"Replaced",
format!(
"binary at `{}`",
dst.strip_prefix(env::current_dir()?)?.display()
),
);
print("Replaced", format!("binary at `{}`", display_path(&dst)));
} else {
print(
"Copied",
format!(
"binary to `{}`",
dst.strip_prefix(env::current_dir()?)?.display()
),
);
print("Copied", format!("binary to `{}`", display_path(&dst)));
}
}

Expand All @@ -137,9 +149,9 @@ fn find_link(workflow_dir: &Path, workflows_dir: &Path) -> Result<Option<PathBuf
}

/// Link the workflow.
fn link(force: bool) -> Result<()> {
let metadata = cargo::metadata()?;
let workflow_dir = metadata.workspace_dir.join("workflow");
fn link(package: Option<&str>, force: bool) -> Result<()> {
let metadata = cargo::metadata(package)?;
let workflow_dir = metadata.manifest_dir.join("workflow");
let workflows_dir = alfred::workflows_directory()?;

if let Some(path) = find_link(&workflow_dir, &workflows_dir)? {
Expand Down Expand Up @@ -168,9 +180,9 @@ fn link(force: bool) -> Result<()> {
}

/// Package the workflow into a `.alfredworkflow` file.
fn package() -> Result<()> {
let metadata = cargo::metadata()?;
let workflow_dir = metadata.workspace_dir.join("workflow");
fn build_package(package: Option<&str>) -> Result<()> {
let metadata = cargo::metadata(package)?;
let workflow_dir = metadata.manifest_dir.join("workflow");
let dist_dir = metadata.target_dir.join("workflow");
let mut package_name = metadata.package_name;

Expand All @@ -183,19 +195,19 @@ fn package() -> Result<()> {

fs::create_dir_all(&dist_dir)?;
alfred::package(&workflow_dir, dst)?;
print(
"Packaged",
format!(
"workflow at `{}`",
dst.strip_prefix(env::current_dir()?)
.unwrap_or(dst)
.display()
),
);
print("Packaged", format!("workflow at `{}`", display_path(dst)));

Ok(())
}

/// Displays a path relative to the current working directory.
fn display_path(path: &Path) -> impl fmt::Display + '_ {
let Ok(cwd) = env::current_dir() else {
return path.display();
};
path.strip_prefix(cwd).unwrap_or(path).display()
}

#[derive(Debug, Parser)]
enum Command {
/// Create a new Rust alfred workflow.
Expand All @@ -218,6 +230,10 @@ enum Command {

/// Build the workflow.
Build {
/// Package to build.
#[clap(long, short, value_name = "SPEC")]
package: Option<String>,

/// Build only the specified binary.
#[clap(long, value_name = "NAME")]
bin: Vec<String>,
Expand All @@ -233,13 +249,21 @@ enum Command {

/// Symlink the workflow directory to the Alfred workflow directory.
Link {
/// Package to build.
#[clap(long, short, value_name = "SPEC")]
package: Option<String>,

/// Delete original symlink and recreate the symlink.
#[clap(long)]
force: bool,
},

/// Package the workflow as an `.alfredworkflow` file.
Package {
/// Package to build.
#[clap(long, short, value_name = "SPEC")]
package: Option<String>,

/// Package only the specified binary.
#[clap(long, value_name = "NAME")]
bin: Vec<String>,
Expand Down Expand Up @@ -277,18 +301,23 @@ fn main() -> anyhow::Result<()> {
init(path, name)?;
}
Command::Build {
package,
bin,
release,
target,
} => {
build(bin, release, target)?;
build(package.as_deref(), bin, release, target.as_deref())?;
}
Command::Link { force } => {
link(force)?;
Command::Link { package, force } => {
link(package.as_deref(), force)?;
}
Command::Package { bin, target } => {
build(bin, true, target)?;
package()?;
Command::Package {
package,
bin,
target,
} => {
build(package.as_deref(), bin, true, target.as_deref())?;
build_package(package.as_deref())?;
}
}
Ok(())
Expand Down
1 change: 1 addition & 0 deletions examples/hello-alfred/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
workflow/hello-alfred
8 changes: 8 additions & 0 deletions examples/hello-alfred/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "hello-alfred"
version = "0.0.0"
edition = "2021"
publish = false

[dependencies]
powerpack = { path = "../.." }
13 changes: 13 additions & 0 deletions examples/hello-alfred/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# hello-alfred

This is an example workflow built using `powerpack`.

Use the `build` command to build this example.
```sh
cargo run --package powerpack-cli -- build --package hello-alfred --release
```

Now use the `link` command to symlink to Alfred.
```sh
cargo run --package powerpack-cli -- link --package hello-alfred
```
38 changes: 38 additions & 0 deletions examples/hello-alfred/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::env;
use std::error::Error;
use std::io;
use std::time::Duration;

use powerpack::*;

fn main() -> Result<(), Box<dyn Error>> {
// Alfred passes in a single argument for the user query.
let query = env::args().nth(1);

// Create an item to show in the Alfred drop down.
let item = Item::new("Hello World!")
.subtitle(format!("Your query was '{query:?}'"))
.uid("unique identifier")
.arg("/path/to/file.jpg")
.icon(Icon::with_type("public.jpeg"))
.valid(true)
.matches("use this to filter")
.autocomplete("to this")
.kind(Kind::FileSkipCheck)
.copy_text("this text will be copied with ⌘C")
.large_type_text("this text will be displayed with ⌘L")
.modifier(Modifier::new(Key::Command).subtitle("⌘ changes the subtitle"))
.modifier(Modifier::new(Key::Option).arg("/path/to/modified.jpg"))
.modifier(Modifier::new(Key::Control).icon(Icon::with_image("/path/to/file.png")))
.modifier(Modifier::new(Key::Shift).valid(false))
.quicklook_url("https://example.com");

// Output the item to Alfred!
Output::new()
.rerun(Duration::from_secs(1))
.skip_knowledge(true)
.items([item])
.write(io::BufWriter::new(io::stdout()))?;

Ok(())
}
Loading

0 comments on commit 32b8bf1

Please sign in to comment.