Skip to content

Commit c8f18e9

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 1474f4d commit c8f18e9

10 files changed

Lines changed: 688 additions & 104 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: 45 additions & 5 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,9 @@ pub trait Storage: Send + Sync {
169172
pub struct RemoteStorage {
170173
operator: opendal::Operator,
171174
basedirs: Vec<Vec<u8>>,
175+
/// When true, use flat keys (no `a/b/c/` prefix). Required for backends
176+
/// like Vercel Artifacts where the key must be a plain hex hash.
177+
flat_keys: bool,
172178
}
173179

174180
#[cfg(any(
@@ -184,7 +190,27 @@ pub struct RemoteStorage {
184190
))]
185191
impl RemoteStorage {
186192
pub fn new(operator: opendal::Operator, basedirs: Vec<Vec<u8>>) -> Self {
187-
Self { operator, basedirs }
193+
Self {
194+
operator,
195+
basedirs,
196+
flat_keys: false,
197+
}
198+
}
199+
200+
pub fn new_flat(operator: opendal::Operator, basedirs: Vec<Vec<u8>>) -> Self {
201+
Self {
202+
operator,
203+
basedirs,
204+
flat_keys: true,
205+
}
206+
}
207+
208+
fn key_path(&self, key: &str) -> String {
209+
if self.flat_keys {
210+
key.to_string()
211+
} else {
212+
normalize_key(key)
213+
}
188214
}
189215
}
190216

@@ -203,7 +229,7 @@ impl RemoteStorage {
203229
#[async_trait]
204230
impl Storage for RemoteStorage {
205231
async fn get(&self, key: &str) -> Result<Cache> {
206-
match self.operator.read(&normalize_key(key)).await {
232+
match self.operator.read(&self.key_path(key)).await {
207233
Ok(res) => {
208234
let hit = CacheRead::from(io::Cursor::new(res.to_bytes()))?;
209235
Ok(Cache::Hit(hit))
@@ -306,7 +332,7 @@ impl Storage for RemoteStorage {
306332
/// which would corrupt the cache entry when written to another level.
307333
async fn get_raw(&self, key: &str) -> Result<Option<Vec<u8>>> {
308334
trace!("opendal::Operator::get_raw({})", key);
309-
match self.operator.read(&normalize_key(key)).await {
335+
match self.operator.read(&self.key_path(key)).await {
310336
Ok(res) => {
311337
let data = res.to_vec();
312338
trace!(
@@ -337,7 +363,7 @@ impl Storage for RemoteStorage {
337363
trace!("opendal::Operator::put_raw({}, {} bytes)", key, data.len());
338364
let start = std::time::Instant::now();
339365

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

342368
Ok(start.elapsed())
343369
}
@@ -542,6 +568,20 @@ pub fn build_single_cache(
542568
let storage = RemoteStorage::new(operator, basedirs.to_vec());
543569
Ok(Arc::new(storage))
544570
}
571+
#[cfg(feature = "vercel_artifacts")]
572+
CacheType::VercelArtifacts(c) => {
573+
debug!("Init vercel artifacts cache");
574+
575+
let operator = VercelArtifactsCache::build(
576+
&c.access_token,
577+
c.endpoint.as_deref(),
578+
c.team_id.as_deref(),
579+
c.team_slug.as_deref(),
580+
)
581+
.map_err(|err| anyhow!("create vercel artifacts cache failed: {err:?}"))?;
582+
let storage = RemoteStorage::new_flat(operator, basedirs.to_vec());
583+
Ok(Arc::new(storage))
584+
}
545585
#[allow(unreachable_patterns)]
546586
_ => {
547587
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
/// A cache that stores entries in Vercel Artifacts.
10+
pub struct VercelArtifactsCache;
11+
12+
impl VercelArtifactsCache {
13+
pub fn build(
14+
access_token: &str,
15+
endpoint: Option<&str>,
16+
team_id: Option<&str>,
17+
team_slug: Option<&str>,
18+
) -> Result<Operator> {
19+
let mut builder = VercelArtifacts::default().access_token(access_token);
20+
if let Some(endpoint) = endpoint {
21+
builder = builder.endpoint(endpoint);
22+
}
23+
if let Some(team_id) = team_id {
24+
builder = builder.team_id(team_id);
25+
}
26+
if let Some(team_slug) = team_slug {
27+
builder = builder.team_slug(team_slug);
28+
}
29+
30+
let op = Operator::new(builder)?
31+
.layer(HttpClientLayer::new(set_user_agent()))
32+
.layer(LoggingLayer::default())
33+
.finish();
34+
Ok(op)
35+
}
36+
}

src/config.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,15 @@ pub struct GHACacheConfig {
349349
pub version: String,
350350
}
351351

352+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
353+
#[serde(deny_unknown_fields)]
354+
pub struct VercelArtifactsCacheConfig {
355+
pub access_token: String,
356+
pub endpoint: Option<String>,
357+
pub team_id: Option<String>,
358+
pub team_slug: Option<String>,
359+
}
360+
352361
/// Memcached's default value of expiration is 10800s (3 hours), which is too
353362
/// short for use case of sccache.
354363
///
@@ -484,6 +493,7 @@ pub enum CacheType {
484493
Webdav(WebdavCacheConfig),
485494
OSS(OSSCacheConfig),
486495
COS(COSCacheConfig),
496+
VercelArtifacts(VercelArtifactsCacheConfig),
487497
}
488498

489499
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
@@ -499,6 +509,7 @@ pub struct CacheConfigs {
499509
pub webdav: Option<WebdavCacheConfig>,
500510
pub oss: Option<OSSCacheConfig>,
501511
pub cos: Option<COSCacheConfig>,
512+
pub vercel_artifacts: Option<VercelArtifactsCacheConfig>,
502513
/// Multi-level cache configuration
503514
pub multilevel: Option<MultiLevelConfig>,
504515
}
@@ -518,6 +529,7 @@ impl CacheConfigs {
518529
webdav,
519530
oss,
520531
cos,
532+
vercel_artifacts,
521533
multilevel: _,
522534
} = self;
523535

@@ -530,7 +542,8 @@ impl CacheConfigs {
530542
.or_else(|| azure.map(CacheType::Azure))
531543
.or_else(|| webdav.map(CacheType::Webdav))
532544
.or_else(|| oss.map(CacheType::OSS))
533-
.or_else(|| cos.map(CacheType::COS));
545+
.or_else(|| cos.map(CacheType::COS))
546+
.or_else(|| vercel_artifacts.map(CacheType::VercelArtifacts));
534547

535548
let fallback = disk.unwrap_or_default();
536549

@@ -576,6 +589,13 @@ impl CacheConfigs {
576589
"oss" => self.oss.clone().map(CacheType::OSS).ok_or_else(|| {
577590
anyhow!("OSS cache not configured but specified in levels")
578591
})?,
592+
"vercel_artifacts" => self
593+
.vercel_artifacts
594+
.clone()
595+
.map(CacheType::VercelArtifacts)
596+
.ok_or_else(|| {
597+
anyhow!("Vercel Artifacts cache not configured but specified in levels")
598+
})?,
579599
"disk" => {
580600
// Disk cache is handled separately in MultiLevelStorage::from_config
581601
// Mark it by continuing - it will be added to the storage list there
@@ -606,6 +626,7 @@ impl CacheConfigs {
606626
webdav,
607627
oss,
608628
cos,
629+
vercel_artifacts,
609630
multilevel,
610631
} = other;
611632

@@ -639,6 +660,9 @@ impl CacheConfigs {
639660
if cos.is_some() {
640661
self.cos = cos;
641662
}
663+
if vercel_artifacts.is_some() {
664+
self.vercel_artifacts = vercel_artifacts;
665+
}
642666

643667
if multilevel.is_some() {
644668
self.multilevel = multilevel;
@@ -1092,6 +1116,21 @@ fn config_from_env() -> Result<EnvConfig> {
10921116
None
10931117
};
10941118

1119+
// ======= Vercel Artifacts =======
1120+
let vercel_artifacts = if let Ok(access_token) = env::var("SCCACHE_VERCEL_ARTIFACTS_TOKEN") {
1121+
let endpoint = env::var("SCCACHE_VERCEL_ARTIFACTS_ENDPOINT").ok();
1122+
let team_id = env::var("SCCACHE_VERCEL_ARTIFACTS_TEAM_ID").ok();
1123+
let team_slug = env::var("SCCACHE_VERCEL_ARTIFACTS_TEAM_SLUG").ok();
1124+
Some(VercelArtifactsCacheConfig {
1125+
access_token,
1126+
endpoint,
1127+
team_id,
1128+
team_slug,
1129+
})
1130+
} else {
1131+
None
1132+
};
1133+
10951134
// ======= Local =======
10961135
let disk_dir = env::var_os("SCCACHE_DIR").map(PathBuf::from);
10971136
let disk_sz = env::var("SCCACHE_CACHE_SIZE")
@@ -1162,6 +1201,7 @@ fn config_from_env() -> Result<EnvConfig> {
11621201
webdav,
11631202
oss,
11641203
cos,
1204+
vercel_artifacts,
11651205
multilevel,
11661206
};
11671207

@@ -2362,6 +2402,7 @@ key_prefix = "cosprefix"
23622402
endpoint: Some("cos.na-siliconvalley.myqcloud.com".to_owned()),
23632403
key_prefix: "cosprefix".into(),
23642404
}),
2405+
vercel_artifacts: None,
23652406
multilevel: None,
23662407
},
23672408
dist: DistConfig {

tests/harness/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ pub fn sccache_client_cfg(
181181
webdav: None,
182182
oss: None,
183183
cos: None,
184+
vercel_artifacts: None,
184185
multilevel: None,
185186
},
186187
dist: sccache::config::DistConfig {

0 commit comments

Comments
 (0)