Skip to content

Commit 3b94df1

Browse files
mmastracclaude
andcommitted
feat: upgrade opendal + add Vercel Artifacts cache backend
Upgrades opendal to mmastrac/opendal vercel_opts branch via [patch.crates-io], which tracks apache/opendal main plus additional Vercel Artifacts builder methods (endpoint, team_id, team_slug) from apache/opendal#7334. Adds layers-logging feature to retain LoggingLayer after upstream split. Adds aws-lc-sys with prebuilt-nasm and bindgen features to avoid nasm/bindgen build requirements in CI cross-compilation. Adds Vercel Artifacts cache backend configured via: - SCCACHE_VERCEL_ARTIFACTS_TOKEN (required) - SCCACHE_VERCEL_ARTIFACTS_ENDPOINT (optional) - SCCACHE_VERCEL_ARTIFACTS_TEAM_ID (optional) - SCCACHE_VERCEL_ARTIFACTS_TEAM_SLUG (optional) Upstream: apache/opendal#7334 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b1f2c15 commit 3b94df1

10 files changed

Lines changed: 720 additions & 107 deletions

File tree

Cargo.lock

Lines changed: 517 additions & 94 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ strip = true
3131
anyhow = { version = "1.0", features = ["backtrace"] }
3232
ar = "0.9"
3333
async-trait = "0.1"
34+
aws-lc-sys = { version = "0.39.1", features = ["prebuilt-nasm"] }
3435
backon = { version = "1", default-features = false, features = [
3536
"std-blocking-sleep",
3637
] }
@@ -69,7 +70,9 @@ memmap2 = "0.9.4"
6970
mime = "0.3"
7071
number_prefix = "0.4"
7172
object = "0.37"
72-
opendal = { version = "0.55.0", optional = true, default-features = false }
73+
opendal = { version = "0.55.0", optional = true, default-features = false, features = [
74+
"layers-logging",
75+
] }
7376
openssl = { version = "0.10.75", optional = true }
7477
rand = "0.8.4"
7578
regex = "1.10.3"
@@ -167,6 +170,7 @@ all = [
167170
"webdav",
168171
"oss",
169172
"cos",
173+
"vercel_artifacts",
170174
]
171175
azure = ["opendal/services-azblob", "reqsign", "reqwest"]
172176
cos = ["opendal/services-cos", "reqsign", "reqwest"]
@@ -178,6 +182,7 @@ native-zlib = []
178182
oss = ["opendal/services-oss", "reqsign", "reqwest"]
179183
redis = ["url", "opendal/services-redis"]
180184
s3 = ["opendal/services-s3", "reqsign", "reqwest"]
185+
vercel_artifacts = ["opendal/services-vercel-artifacts", "reqwest"]
181186
webdav = ["opendal/services-webdav", "reqwest"]
182187
# Enable features that will build a vendored version of openssl and
183188
# statically linked with it, instead of linking against the system-wide openssl
@@ -233,3 +238,6 @@ ptr_as_ptr = "warn"
233238
ref_option = "warn"
234239
semicolon_if_nothing_returned = "warn"
235240
unnecessary_semicolon = "warn"
241+
242+
[patch.crates-io]
243+
opendal = { git = "https://github.com/mmastrac/opendal.git", rev = "723ba7f7359e", package = "opendal" }

docs/Configuration.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,10 @@ The full url appears then as `redis://user:passwd@1.2.3.4:6379/?db=1`.
310310
* `SCCACHE_COS_KEY_PREFIX`
311311
* `TENCENTCLOUD_SECRET_ID`
312312
* `TENCENTCLOUD_SECRET_KEY`
313+
314+
#### Vercel Artifacts
315+
316+
* `SCCACHE_VERCEL_ARTIFACTS_TOKEN` Vercel access token for the artifacts API (required)
317+
* `SCCACHE_VERCEL_ARTIFACTS_ENDPOINT` API endpoint (default: `https://api.vercel.com`)
318+
* `SCCACHE_VERCEL_ARTIFACTS_TEAM_ID` Vercel team ID, appended as `teamId` query parameter
319+
* `SCCACHE_VERCEL_ARTIFACTS_TEAM_SLUG` Vercel team slug, appended as `slug` query parameter

src/cache/cache.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@ use crate::cache::s3::S3Cache;
4040
feature = "s3",
4141
feature = "webdav",
4242
feature = "oss",
43-
feature = "cos"
43+
feature = "cos",
44+
feature = "vercel_artifacts"
4445
))]
4546
use crate::cache::utils::normalize_key;
47+
#[cfg(feature = "vercel_artifacts")]
48+
use crate::cache::vercel_artifacts::VercelArtifactsCache;
4649
#[cfg(feature = "webdav")]
4750
use crate::cache::webdav::WebdavCache;
4851
use crate::compiler::PreprocessorCacheEntry;
@@ -169,6 +172,10 @@ pub trait Storage: Send + Sync {
169172
pub struct RemoteStorage {
170173
operator: opendal::Operator,
171174
basedirs: Vec<Vec<u8>>,
175+
/// Optional transform applied to every key (including health-check paths)
176+
/// before it is sent to the operator. Used by backends like Vercel Artifacts
177+
/// that only accept alphanumeric artifact IDs.
178+
key_transform: Option<fn(&str) -> String>,
172179
}
173180

174181
#[cfg(any(
@@ -184,7 +191,31 @@ pub struct RemoteStorage {
184191
))]
185192
impl RemoteStorage {
186193
pub fn new(operator: opendal::Operator, basedirs: Vec<Vec<u8>>) -> Self {
187-
Self { operator, basedirs }
194+
Self {
195+
operator,
196+
basedirs,
197+
key_transform: None,
198+
}
199+
}
200+
201+
pub fn new_with_key_transform(
202+
operator: opendal::Operator,
203+
basedirs: Vec<Vec<u8>>,
204+
key_transform: fn(&str) -> String,
205+
) -> Self {
206+
Self {
207+
operator,
208+
basedirs,
209+
key_transform: Some(key_transform),
210+
}
211+
}
212+
213+
fn key_path(&self, key: &str) -> String {
214+
let normalized = normalize_key(key);
215+
match self.key_transform {
216+
Some(transform) => transform(&normalized),
217+
None => normalized,
218+
}
188219
}
189220
}
190221

@@ -203,7 +234,7 @@ impl RemoteStorage {
203234
#[async_trait]
204235
impl Storage for RemoteStorage {
205236
async fn get(&self, key: &str) -> Result<Cache> {
206-
match self.operator.read(&normalize_key(key)).await {
237+
match self.operator.read(&self.key_path(key)).await {
207238
Ok(res) => {
208239
let hit = CacheRead::from(io::Cursor::new(res.to_bytes()))?;
209240
Ok(Cache::Hit(hit))
@@ -226,10 +257,10 @@ impl Storage for RemoteStorage {
226257
async fn check(&self) -> Result<CacheMode> {
227258
use opendal::ErrorKind;
228259

229-
let path = ".sccache_check";
260+
let path = self.key_path(".sccache_check");
230261

231262
// Read is required, return error directly if we can't read .
232-
match self.operator.read(path).await {
263+
match self.operator.read(&path).await {
233264
Ok(_) => (),
234265
// Read not exist file with not found is ok.
235266
Err(err) if err.kind() == ErrorKind::NotFound => (),
@@ -248,7 +279,7 @@ impl Storage for RemoteStorage {
248279
Err(err) => bail!("cache storage failed to read: {:?}", err),
249280
}
250281

251-
let can_write = match self.operator.write(path, "Hello, World!").await {
282+
let can_write = match self.operator.write(&path, "Hello, World!").await {
252283
Ok(_) => true,
253284
Err(err) if err.kind() == ErrorKind::AlreadyExists => true,
254285
// Tolerate all other write errors because we can do read at least.
@@ -306,7 +337,7 @@ impl Storage for RemoteStorage {
306337
/// which would corrupt the cache entry when written to another level.
307338
async fn get_raw(&self, key: &str) -> Result<Option<Vec<u8>>> {
308339
trace!("opendal::Operator::get_raw({})", key);
309-
match self.operator.read(&normalize_key(key)).await {
340+
match self.operator.read(&self.key_path(key)).await {
310341
Ok(res) => {
311342
let data = res.to_vec();
312343
trace!(
@@ -337,7 +368,7 @@ impl Storage for RemoteStorage {
337368
trace!("opendal::Operator::put_raw({}, {} bytes)", key, data.len());
338369
let start = std::time::Instant::now();
339370

340-
self.operator.write(&normalize_key(key), data).await?;
371+
self.operator.write(&self.key_path(key), data).await?;
341372

342373
Ok(start.elapsed())
343374
}
@@ -542,6 +573,24 @@ pub fn build_single_cache(
542573
let storage = RemoteStorage::new(operator, basedirs.to_vec());
543574
Ok(Arc::new(storage))
544575
}
576+
#[cfg(feature = "vercel_artifacts")]
577+
CacheType::VercelArtifacts(c) => {
578+
debug!("Init vercel artifacts cache");
579+
580+
let operator = VercelArtifactsCache::build(
581+
&c.access_token,
582+
c.endpoint.as_deref(),
583+
c.team_id.as_deref(),
584+
c.team_slug.as_deref(),
585+
)
586+
.map_err(|err| anyhow!("create vercel artifacts cache failed: {err:?}"))?;
587+
let storage = RemoteStorage::new_with_key_transform(
588+
operator,
589+
basedirs.to_vec(),
590+
crate::cache::vercel_artifacts::sanitize_key,
591+
);
592+
Ok(Arc::new(storage))
593+
}
545594
#[allow(unreachable_patterns)]
546595
_ => {
547596
bail!("Cache type not supported with current feature configuration")

src/cache/memcached.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ use opendal::services::Memcached;
2121

2222
use crate::errors::*;
2323

24+
/// Resolve hostname in a memcached endpoint URL to an IP address.
25+
/// The new opendal memcached service uses SocketAddr::parse internally which
26+
/// doesn't support DNS hostnames, only IP:port. This works around that by
27+
/// resolving tcp://hostname:port to tcp://ip:port.
28+
fn resolve_memcached_endpoint(url: &str) -> String {
29+
if let Some(rest) = url.strip_prefix("tcp://") {
30+
if let Ok(addrs) = std::net::ToSocketAddrs::to_socket_addrs(&rest) {
31+
if let Some(addr) = addrs.into_iter().next() {
32+
return format!("tcp://{}", addr);
33+
}
34+
}
35+
}
36+
url.to_string()
37+
}
38+
2439
#[derive(Clone)]
2540
pub struct MemcachedCache;
2641

@@ -32,7 +47,10 @@ impl MemcachedCache {
3247
key_prefix: &str,
3348
expiration: u32,
3449
) -> Result<Operator> {
35-
let mut builder = Memcached::default().endpoint(url);
50+
// The new opendal memcached service uses SocketAddr::parse which doesn't
51+
// support hostnames. Resolve hostname to IP if the endpoint uses tcp://.
52+
let url = resolve_memcached_endpoint(url);
53+
let mut builder = Memcached::default().endpoint(&url);
3654

3755
if let Some(username) = username {
3856
builder = builder.username(username);

src/cache/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub mod redis;
3737
#[cfg(feature = "s3")]
3838
pub mod s3;
3939
pub(crate) mod utils;
40+
#[cfg(feature = "vercel_artifacts")]
41+
pub mod vercel_artifacts;
4042
#[cfg(feature = "webdav")]
4143
pub mod webdav;
4244

@@ -47,7 +49,8 @@ pub mod webdav;
4749
feature = "s3",
4850
feature = "webdav",
4951
feature = "oss",
50-
feature = "cos"
52+
feature = "cos",
53+
feature = "vercel_artifacts"
5154
))]
5255
pub(crate) mod http_client;
5356

src/cache/multilevel.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,8 @@ impl MultiLevelStorage {
409409
feature = "s3",
410410
feature = "webdav",
411411
feature = "oss",
412-
feature = "cos"
412+
feature = "cos",
413+
feature = "vercel_artifacts"
413414
))]
414415
{
415416
let cache_type = match level_name.to_lowercase().as_str() {
@@ -435,6 +436,12 @@ impl MultiLevelStorage {
435436
"oss" => config.cache_configs.oss.clone().map(CacheType::OSS),
436437
#[cfg(feature = "cos")]
437438
"cos" => config.cache_configs.cos.clone().map(CacheType::COS),
439+
#[cfg(feature = "vercel_artifacts")]
440+
"vercel_artifacts" => config
441+
.cache_configs
442+
.vercel_artifacts
443+
.clone()
444+
.map(CacheType::VercelArtifacts),
438445
_ => {
439446
return Err(anyhow!("Unknown cache level: '{}'", level_name));
440447
}

src/cache/vercel_artifacts.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use opendal::Operator;
2+
use opendal::layers::{HttpClientLayer, LoggingLayer};
3+
use opendal::services::VercelArtifacts;
4+
5+
use crate::errors::*;
6+
7+
use super::http_client::set_user_agent;
8+
9+
/// Sanitize a cache key so it contains only alphanumeric characters [a-zA-Z0-9],
10+
/// as required by the Vercel Artifacts API (the `:hash` route parameter doesn't
11+
/// tolerate `.`, `/`, or other special characters).
12+
///
13+
/// Alphanumeric characters are passed through unchanged. Every other byte is
14+
/// replaced with two uppercase hex digits (e.g. `/` → `2F`, `.` → `2E`).
15+
/// This is reversible and keeps already-valid hex hash keys untouched.
16+
pub fn sanitize_key(key: &str) -> String {
17+
let mut out = String::with_capacity(key.len());
18+
for b in key.bytes() {
19+
if b.is_ascii_alphanumeric() {
20+
out.push(b as char);
21+
} else {
22+
out.push(char::from_digit((b >> 4) as u32, 16).unwrap().to_ascii_uppercase());
23+
out.push(char::from_digit((b & 0xf) as u32, 16).unwrap().to_ascii_uppercase());
24+
}
25+
}
26+
out
27+
}
28+
29+
/// A cache that stores entries in Vercel Artifacts.
30+
pub struct VercelArtifactsCache;
31+
32+
impl VercelArtifactsCache {
33+
pub fn build(
34+
access_token: &str,
35+
endpoint: Option<&str>,
36+
team_id: Option<&str>,
37+
team_slug: Option<&str>,
38+
) -> Result<Operator> {
39+
let mut builder = VercelArtifacts::default().access_token(access_token);
40+
if let Some(endpoint) = endpoint {
41+
builder = builder.endpoint(endpoint);
42+
}
43+
if let Some(team_id) = team_id {
44+
builder = builder.team_id(team_id);
45+
}
46+
if let Some(team_slug) = team_slug {
47+
builder = builder.team_slug(team_slug);
48+
}
49+
50+
let op = Operator::new(builder)?
51+
.layer(HttpClientLayer::new(set_user_agent()))
52+
.layer(LoggingLayer::default())
53+
.finish();
54+
Ok(op)
55+
}
56+
}

0 commit comments

Comments
 (0)