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
11 changes: 8 additions & 3 deletions scarb/src/bin/scarb/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,10 @@ pub enum Command {
ProcMacroServer,
/// Upload a package to the registry.
#[command(after_help = "\
This command will create distributable, compressed `.tar.zst` archive containing source \
code of the package in `target/package` directory (using `scarb package`) and upload it \
to a registry.
This command will create a distributable, compressed `.tar.zst` archive \
containing the package's source code in the `target/package` directory \
(using `scarb package`), generate its documentation, and upload both to a \
registry. Use `--no-docs` to skip documentation generation and upload.
")]
Publish(PublishArgs),
/// Checks a package to catch common mistakes and improve your Cairo code.
Expand Down Expand Up @@ -562,6 +563,10 @@ pub struct PublishArgs {
/// Do not error on `cairo-version` mismatch.
#[arg(long, env = "SCARB_IGNORE_CAIRO_VERSION")]
pub ignore_cairo_version: bool,

/// Do not generate and upload documentation.
#[arg(long, env = "SCARB_PUBLISH_NO_DOCS")]
pub no_docs: bool,
}

/// Arguments accepted by the `lint` command.
Expand Down
1 change: 1 addition & 0 deletions scarb/src/bin/scarb/commands/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub fn run(args: PublishArgs, config: &Config) -> Result<()> {
features: features_opts,
ignore_cairo_version: args.ignore_cairo_version,
},
docs: !args.no_docs,
};

ops::publish(package.id, &ops, &ws)
Expand Down
74 changes: 74 additions & 0 deletions scarb/src/core/registry/client/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,80 @@ impl RegistryClient for HttpRegistryClient<'_> {
};
Ok(result)
}

async fn supports_publish_docs(&self) -> Result<bool> {
Ok(self.index_config.load().await?.docs_upload.is_some())
}

async fn publish_docs(
&self,
package: PackageId,
tarball: LockedFile,
force: bool,
) -> Result<RegistryUpload> {
let auth_token = env::var("SCARB_REGISTRY_AUTH_TOKEN").map_err(|_| {
anyhow!(
"missing authentication token. \
help: make sure SCARB_REGISTRY_AUTH_TOKEN environment variable is set"
)
})?;

let index_config = self.index_config.load().await?;
let mut docs_upload_url = match &index_config.docs_upload {
Some(url) => url.expand(package.into())?,
None => {
return Ok(RegistryUpload::Failure(anyhow!(
"registry does not support docs upload"
)));
}
};

if force {
docs_upload_url.set_query(Some("force=true"));
}

let file = tarball.into_async();

let file_part = Part::stream(Body::from(file.try_clone().await?))
.file_name(format!("docs_{}_{}", package.name, package.version));
let form = Form::new().part("file", file_part);
Comment thread
hakiers marked this conversation as resolved.

let response = self
.config
.online_http()?
.post(docs_upload_url)
.header(AUTHORIZATION, format!("Bearer {auth_token}"))
.multipart(form)
.send()
.await?;

let result = match response.status() {
StatusCode::OK => RegistryUpload::Success,
status => {
let headers = response.headers().clone();
let error_body: serde_json::Value = response.json().await.unwrap_or_default();
let error_message = error_body
.get("error")
.and_then(|e| e.as_str())
.unwrap_or("missing error field in the registry response");

let trace_id = headers
.get("x-cloud-trace-context")
.and_then(|v| v.to_str().ok());

let error_message = match trace_id {
Some(id) => format!(
"upload failed with status code: `{status}`, `{error_message}` (trace-id: {id:?})"
),
None => {
format!("upload failed with status code: `{status}`, `{error_message}`",)
}
};
RegistryUpload::Failure(anyhow!(error_message))
}
};
Ok(result)
}
}

impl HttpCacheKey {
Expand Down
17 changes: 16 additions & 1 deletion scarb/src/core/registry/client/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::io::{BufReader, BufWriter, Seek, SeekFrom};
use std::ops::Deref;
use std::path::{Path, PathBuf};

use anyhow::{Context, Error, Result, ensure};
use anyhow::{Context, Error, Result, anyhow, ensure};
use async_trait::async_trait;
use tokio::task::spawn_blocking;
use tracing::trace;
Expand Down Expand Up @@ -159,6 +159,21 @@ impl RegistryClient for LocalRegistryClient<'_> {
.await
.with_context(|| format!("failed to publish package: {package}"))?
}

async fn supports_publish_docs(&self) -> Result<bool> {
Ok(false)
}

async fn publish_docs(
&self,
_package: PackageId,
_tarball: LockedFile,
_force: bool,
) -> Result<RegistryUpload> {
Ok(RegistryUpload::Failure(anyhow!(
"local registry does not support docs upload"
)))
}
}

fn publish_impl(
Expand Down
15 changes: 15 additions & 0 deletions scarb/src/core/registry/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,19 @@ pub trait RegistryClient: Send + Sync {
/// The client is free to use information within `package` to send to the registry.
/// Package source is not required to match the registry the package is published to.
async fn publish(&self, package: Package, tarball: LockedFile) -> Result<RegistryUpload>;

/// State whether documentation can be published to this registry.
async fn supports_publish_docs(&self) -> Result<bool> {
Ok(false)
}

/// Publish documentation for a package to this registry.
///
/// This function can only be called if [`RegistryClient::supports_publish_docs`] returns `true`.
async fn publish_docs(
&self,
package: PackageId,
tarball: LockedFile,
force: bool,
) -> Result<RegistryUpload>;
}
14 changes: 12 additions & 2 deletions scarb/src/core/registry/index/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use crate::core::registry::index::{BaseUrl, TemplateUrl};
/// "api": "https://example.com/api/v1",
/// "dl": "https://example.com/api/v1/download/{package}/{version}",
/// "upload": "https://example.com/api/v1/packages/new",
/// "index": "https://example.com/index/{prefix}/{package}.json"
/// "index": "https://example.com/index/{prefix}/{package}.json",
/// "docs-upload": "https://example.com/api/v1/docs/{package}/{version}"
/// }
/// ```
///
Expand Down Expand Up @@ -48,6 +49,11 @@ pub struct IndexConfig {
///
/// If this is `None`, the registry does not support package uploads.
pub upload: Option<Url>,

/// Docs upload endpoint for all docs.
///
/// If this is `None`, the registry does not support docs uploads.
pub docs_upload: Option<TemplateUrl>,
}

impl IndexConfig {
Expand Down Expand Up @@ -87,6 +93,9 @@ mod tests {
upload: Some("https://example.com/api/v1/packages/new".parse().unwrap()),
dl: TemplateUrl::new("https://example.com/api/v1/download/{package}/{version}"),
index: TemplateUrl::new("https://example.com/index/{prefix}/{package}.json"),
docs_upload: Some(TemplateUrl::new(
"https://example.com/api/v1/docs/{package}/{version}",
)),
};

let actual: IndexConfig = serde_json::from_str(
Expand All @@ -95,7 +104,8 @@ mod tests {
"api": "https://example.com/api/v1",
"upload": "https://example.com/api/v1/packages/new",
"dl": "https://example.com/api/v1/download/{package}/{version}",
"index": "https://example.com/index/{prefix}/{package}.json"
"index": "https://example.com/index/{prefix}/{package}.json",
"docs-upload": "https://example.com/api/v1/docs/{package}/{version}"
}"#,
)
.unwrap();
Expand Down
90 changes: 90 additions & 0 deletions scarb/src/ops/docs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use crate::core::PackageId;
use crate::core::Workspace;
use crate::flock::LockedFile;
use crate::ops::subcommands::execute_external_subcommand_and_wait;
use anyhow::{Context, Result};
use camino::Utf8Path;
use camino::Utf8PathBuf;
use scarb_ui::HumanBytes;
use scarb_ui::HumanCount;
use scarb_ui::components::Status;
use std::fs::File;
use std::io::Seek;
use std::io::SeekFrom;

fn generate_docs(pkg_id: &PackageId, ws: &Workspace<'_>) -> Result<Utf8PathBuf> {
let docs_target = ws.target_dir().path_unchecked().to_owned();
let config = ws.config();
let args = vec![
"--build".into(),
"--disable-remote-linking".into(),
"--package".into(),
pkg_id.name.to_string().into(),
];

execute_external_subcommand_and_wait("doc", &args, None, config, Some(docs_target.clone()))?;

let docs_path = docs_target
.join("doc")
.join(pkg_id.name.to_string())
.join("book");
Ok(docs_path)
}

fn tar(src: &Utf8Path, dst: &mut File) -> Result<()> {
const COMPRESSION_LEVEL: i32 = 22;
let encoder = zstd::stream::Encoder::new(dst, COMPRESSION_LEVEL)?;

let mut tar = tar::Builder::new(encoder);
tar.append_dir_all(".", src)
.with_context(|| format!("failed to append directory all for {}", src))?;

let encoder = tar.into_inner()?;

encoder.finish()?;
Ok(())
}

fn dir_stats(src: &Utf8Path) -> Result<(usize, u64)> {
let mut count = 0;
let mut size = 0;
for entry in walkdir::WalkDir::new(src) {
let entry = entry?;
if entry.file_type().is_file() {
count += 1;
size += entry.metadata()?.len();
}
}
Ok((count, size))
}

pub fn package_docs_one(package_id: &PackageId, ws: &Workspace<'_>) -> Result<LockedFile> {
let docs_path = generate_docs(package_id, ws)?;

let filename = format!("docs.{}", package_id.tarball_name());
let target_dir = ws.target_dir().child("doc");

let mut dst = target_dir.create_rw(&filename, "docs tarball", ws.config())?;

tar(&docs_path, &mut dst)?;

dst.seek(SeekFrom::Start(0))?;
let dst_metadata = dst
.metadata()
.with_context(|| format!("failed to get metadata for {}", dst.path()))?;

let (num_files, uncompressed_size) = dir_stats(&docs_path)?;
let compressed_size = dst_metadata.len();

ws.config().ui().print(Status::new(
"Packaged",
&format!(
"docs {} files, {:.1} ({:.1} compressed)",
HumanCount(num_files as u64),
HumanBytes(uncompressed_size),
HumanBytes(compressed_size),
),
));

Ok(dst)
}
2 changes: 2 additions & 0 deletions scarb/src/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
pub use cache::*;
pub use clean::*;
pub use compile::*;
pub use docs::*;
pub use expand::*;
pub use fmt::*;
pub use manifest::*;
Expand All @@ -21,6 +22,7 @@ pub use workspace::*;
mod cache;
mod clean;
mod compile;
mod docs;
mod expand;
mod fmt;
mod lockfile;
Expand Down
35 changes: 35 additions & 0 deletions scarb/src/ops/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use super::PackageOpts;
pub struct PublishOpts {
pub index_url: Url,
pub package_opts: PackageOpts,
pub docs: bool,
}

#[tracing::instrument(level = "debug", skip(opts, ws))]
Expand Down Expand Up @@ -43,7 +44,20 @@ pub fn publish(package_id: PackageId, opts: &PublishOpts, ws: &Workspace<'_>) ->
"publishing packages is not supported by registry: {source_id}"
);

let supports_publish_docs = ws
.config()
.tokio_handle()
.block_on(registry_client.supports_publish_docs())
.with_context(|| {
format!("failed to check if registry supports publishing docs: {source_id}")
})?;

let tarball = ops::package_one(package_id, &opts.package_opts, ws)?;
let docs_tarball = if opts.docs && supports_publish_docs {
Some(ops::package_docs_one(&package_id, ws)?)
} else {
None
};

let dest_package_id = package_id.with_source_id(source_id);

Expand All @@ -59,6 +73,27 @@ pub fn publish(package_id: PackageId, opts: &PublishOpts, ws: &Workspace<'_>) ->
"Published",
format!("{}", dest_package_id).as_str(),
));

// Upload docs if they were generated and the registry supports it.
if let Some(docs_tarball) = docs_tarball {
ws.config().ui().print(Status::new(
"Uploading",
&format!("docs for {}", dest_package_id),
));

let upload_docs = registry_client
.publish_docs(package_id, docs_tarball, false)
.await;
match upload_docs {
Ok(RegistryUpload::Success) => {
ws.config().ui().print(Status::new(
"Published",
format!("docs for {}", dest_package_id).as_str(),
));
}
Ok(RegistryUpload::Failure(e)) | Err(e) => return Err(e),
}
}
Ok(())
}
Ok(RegistryUpload::Failure(e)) => Err(e),
Expand Down
Loading
Loading