blob: 68c6c32923345faad406373830df1215acfc157c [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::workspace_dir;
use semver::Version;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use toml_edit::{DocumentMut, Item, TableLike};
#[derive(Debug, Clone)]
pub struct Package {
name: String,
path: PathBuf,
version: Version,
dependencies: Vec<Package>,
}
impl Package {
pub fn dependencies(&self) -> &[Package] {
&self.dependencies
}
pub fn name(&self) -> &str {
&self.name
}
pub fn make_prefix(&self) -> String {
format!(
"apache-opendal-{}-{}-src",
self.name.replace("/", "-"),
self.version
)
}
}
fn make_package(path: &str, version: &str, dependencies: Vec<Package>) -> Package {
let name = path.to_string();
let path = workspace_dir().join(path);
let version = Version::parse(version).unwrap();
Package {
name,
path,
version,
dependencies,
}
}
/// List all packages that are ready for release.
pub fn all_packages() -> Vec<Package> {
let core = make_package("core", "0.56.0", vec![]);
// Integrations
let dav_server = make_package("integrations/dav-server", "0.7.1", vec![core.clone()]);
let object_store = make_package("integrations/object_store", "0.56.0", vec![core.clone()]);
let parquet = make_package("integrations/parquet", "0.8.0", vec![core.clone()]);
let unftp_sbe = make_package("integrations/unftp-sbe", "0.4.1", vec![core.clone()]);
// Binaries moved to separate repositories; no longer released from this repo
// Bindings
let c = make_package("bindings/c", "0.46.5", vec![core.clone()]);
let cpp = make_package("bindings/cpp", "0.45.25", vec![core.clone()]);
let java = make_package("bindings/java", "0.48.3", vec![core.clone()]);
let nodejs = make_package("bindings/nodejs", "0.49.3", vec![core.clone()]);
let python = make_package("bindings/python", "0.47.1", vec![core.clone()]);
vec![
core,
dav_server,
object_store,
parquet,
unftp_sbe,
c,
cpp,
java,
nodejs,
python,
]
}
pub fn update_package_version(package: &Package) -> bool {
match package.name.as_str() {
"core" => update_cargo_version(&package.path, &package.version, package.dependencies()),
"integrations/dav-server" => {
update_cargo_version(&package.path, &package.version, package.dependencies())
}
"integrations/object_store" => {
update_cargo_version(&package.path, &package.version, package.dependencies())
}
"integrations/parquet" => {
update_cargo_version(&package.path, &package.version, package.dependencies())
}
"integrations/unftp-sbe" => {
update_cargo_version(&package.path, &package.version, package.dependencies())
}
"bindings/c" => false, // C bindings has no version to update
"bindings/cpp" => false, // C++ bindings has no version to update
"bindings/lua" => false, // Lua bindings has no version to update
"bindings/python" => update_cargo_version(&package.path, &package.version, &[]),
"bindings/java" => update_maven_version(&package.path, &package.version),
"bindings/nodejs" => update_nodejs_version(&package.path, &package.version),
name => panic!("unknown package: {name}"),
}
}
fn update_cargo_version(path: &Path, version: &Version, dependencies: &[Package]) -> bool {
let manifest_path = path.join("Cargo.toml");
let manifest = std::fs::read_to_string(&manifest_path).unwrap();
let mut manifest = DocumentMut::from_str(manifest.as_str()).unwrap();
let mut updated = update_manifest_version(&mut manifest, version, &manifest_path);
for dependency in dependencies {
updated |= update_dependency_version(&mut manifest, path, dependency);
}
if updated {
std::fs::write(&manifest_path, manifest.to_string()).unwrap();
}
if is_core_workspace_root(path) {
updated |= sync_workspace_internal_dependency_versions(path, version);
}
updated
}
fn update_manifest_version(manifest: &mut DocumentMut, version: &Version, path: &Path) -> bool {
if let Some(old_version) = manifest["package"]["version"]
.as_str()
.map(Version::parse)
.transpose()
.unwrap()
{
if &old_version != version {
manifest["package"]["version"] = toml_edit::value(version.to_string());
println!(
"updating version for package: {} from {} to {}",
path.display(),
old_version,
version
);
return true;
}
return false;
}
if let Some(old_version) = manifest["workspace"]["package"]["version"]
.as_str()
.map(Version::parse)
.transpose()
.unwrap()
{
if &old_version != version {
manifest["workspace"]["package"]["version"] = toml_edit::value(version.to_string());
println!(
"updating version for package: {} from {} to {}",
path.display(),
old_version,
version
);
return true;
}
return false;
}
panic!("missing version for package: {}", path.display());
}
fn update_dependency_version(
manifest: &mut DocumentMut,
manifest_dir: &Path,
dependency: &Package,
) -> bool {
let Some(crate_name) = dependency.crate_name() else {
return false;
};
update_dependency_version_in_table(
manifest.as_table_mut(),
manifest_dir,
crate_name,
dependency,
)
}
fn update_dependency_version_in_table(
table: &mut dyn TableLike,
manifest_dir: &Path,
crate_name: &str,
dependency: &Package,
) -> bool {
let mut updated = false;
for table_name in ["dependencies", "dev-dependencies", "build-dependencies"] {
let Some(dependencies) = table.get_mut(table_name).and_then(Item::as_table_like_mut) else {
continue;
};
updated |= update_dependency_version_in_dependencies(
dependencies,
manifest_dir,
crate_name,
dependency,
);
}
let Some(targets) = table.get_mut("target").and_then(Item::as_table_like_mut) else {
return updated;
};
for (_, target) in targets.iter_mut() {
let Some(target) = target.as_table_like_mut() else {
continue;
};
updated |= update_dependency_version_in_table(target, manifest_dir, crate_name, dependency);
}
updated
}
fn update_dependency_version_in_dependencies(
dependencies: &mut dyn TableLike,
manifest_dir: &Path,
crate_name: &str,
dependency: &Package,
) -> bool {
let Some(entry) = dependencies.get_mut(crate_name) else {
return false;
};
let Some(entry) = entry.as_table_like_mut() else {
return false;
};
let Some(path) = entry.get("path").and_then(Item::as_str) else {
return false;
};
if !path_points_to(manifest_dir, path, &dependency.path) {
return false;
}
let Some(value) = entry.get_mut("version") else {
return false;
};
let old_version = match value.as_str() {
Some(version) => match Version::parse(version) {
Ok(version) => version,
Err(_) => return false,
},
None => panic!("missing dependency version for crate: {crate_name}"),
};
if old_version == dependency.version {
return false;
}
*value = toml_edit::value(dependency.version.to_string());
println!(
"updating dependency version for crate: {} from {} to {}",
crate_name, old_version, dependency.version
);
true
}
fn is_core_workspace_root(path: &Path) -> bool {
path == workspace_dir().join("core")
}
fn sync_workspace_internal_dependency_versions(workspace_root: &Path, version: &Version) -> bool {
let workspace_root = normalize_path(workspace_root);
let mut updated = false;
for entry in ignore::Walk::new(&workspace_root) {
let entry = entry.unwrap();
if entry.file_name() != "Cargo.toml" {
continue;
}
let manifest_path = entry.path();
let manifest_dir = manifest_path.parent().unwrap();
let manifest = std::fs::read_to_string(manifest_path).unwrap();
let mut manifest = DocumentMut::from_str(&manifest).unwrap();
let mut manifest_updated = sync_workspace_internal_dependency_versions_in_table(
manifest.as_table_mut(),
manifest_dir,
&workspace_root,
version,
);
manifest_updated |= sync_workspace_package_version(&mut manifest, version);
if manifest_updated {
std::fs::write(manifest_path, manifest.to_string()).unwrap();
updated = true;
}
}
updated
}
fn sync_workspace_package_version(manifest: &mut DocumentMut, version: &Version) -> bool {
let Some(package) = manifest.get("package").and_then(Item::as_table_like) else {
return false;
};
let Some(name) = package.get("name").and_then(Item::as_str) else {
return false;
};
if !name.starts_with("opendal") {
return false;
}
let Some(old_version) = manifest["package"]["version"]
.as_str()
.map(Version::parse)
.transpose()
.unwrap()
else {
return false;
};
if &old_version == version {
return false;
}
manifest["package"]["version"] = toml_edit::value(version.to_string());
true
}
fn sync_workspace_internal_dependency_versions_in_table(
table: &mut dyn TableLike,
manifest_dir: &Path,
workspace_root: &Path,
version: &Version,
) -> bool {
let mut updated = false;
for table_name in ["dependencies", "dev-dependencies", "build-dependencies"] {
let Some(dependencies) = table.get_mut(table_name).and_then(Item::as_table_like_mut) else {
continue;
};
updated |= sync_workspace_internal_dependency_versions_in_dependencies(
dependencies,
manifest_dir,
workspace_root,
version,
);
}
let Some(targets) = table.get_mut("target").and_then(Item::as_table_like_mut) else {
return updated;
};
for (_, target) in targets.iter_mut() {
let Some(target) = target.as_table_like_mut() else {
continue;
};
updated |= sync_workspace_internal_dependency_versions_in_table(
target,
manifest_dir,
workspace_root,
version,
);
}
updated
}
fn sync_workspace_internal_dependency_versions_in_dependencies(
dependencies: &mut dyn TableLike,
manifest_dir: &Path,
workspace_root: &Path,
version: &Version,
) -> bool {
let mut updated = false;
for (_, dependency) in dependencies.iter_mut() {
let Some(dependency) = dependency.as_table_like_mut() else {
continue;
};
let Some(path) = dependency.get("path").and_then(Item::as_str) else {
continue;
};
if !path_points_into(manifest_dir, path, workspace_root) {
continue;
}
let Some(value) = dependency.get_mut("version") else {
continue;
};
let old_version = match value.as_str() {
Some(version) => match Version::parse(version) {
Ok(version) => version,
Err(_) => continue,
},
None => continue,
};
if &old_version == version {
continue;
}
*value = toml_edit::value(version.to_string());
updated = true;
}
updated
}
fn path_points_to(manifest_dir: &Path, raw_path: &str, expected: &Path) -> bool {
normalize_path(&manifest_dir.join(raw_path)) == normalize_path(expected)
}
fn path_points_into(manifest_dir: &Path, raw_path: &str, workspace_root: &Path) -> bool {
normalize_path(&manifest_dir.join(raw_path)).starts_with(workspace_root)
}
fn normalize_path(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| lexical_normalize(path))
}
fn lexical_normalize(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
normalized.pop();
}
_ => normalized.push(component.as_os_str()),
}
}
normalized
}
impl Package {
fn crate_name(&self) -> Option<&'static str> {
match self.name.as_str() {
"core" => Some("opendal"),
_ => None,
}
}
}
fn update_maven_version(path: &Path, version: &Version) -> bool {
let path = path.join("pom.xml");
let manifest = std::fs::read_to_string(&path).unwrap();
let old_version_matcher = r#"<version>\d+\.\d+\.\d+</version> <!-- update version number -->"#;
let new_version_string = format!("<version>{version}</version> <!-- update version number -->");
let new_manifest = regex::Regex::new(old_version_matcher)
.unwrap()
.replace_all(manifest.as_str(), new_version_string.as_str());
if manifest != new_manifest {
std::fs::write(&path, new_manifest.as_bytes()).unwrap();
println!(
"updating version for package: {} to {}",
path.display(),
version
);
true
} else {
false
}
}
fn update_nodejs_version(path: &Path, version: &Version) -> bool {
let mut updated = false;
for entry in ignore::Walk::new(path) {
let entry = entry.unwrap();
if entry.file_name() != "package.json" {
continue;
}
// Ignore theme/package.json
if entry.path().to_str().unwrap().contains("theme") {
continue;
}
let manifest = std::fs::read_to_string(entry.path()).unwrap();
let mut manifest: serde_json::Value = serde_json::from_str(&manifest).unwrap();
let value = manifest.pointer_mut("/version").unwrap();
let old_version = Version::parse(value.as_str().unwrap()).unwrap();
if &old_version != version {
*value = serde_json::Value::String(version.to_string());
let mut new_manifest = serde_json::to_string_pretty(&manifest).unwrap();
new_manifest.push('\n');
std::fs::write(entry.path(), new_manifest).unwrap();
println!(
"updating version for package: {} from {} to {}",
entry.path().display(),
old_version,
version
);
updated = true;
}
}
updated
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_test_dir(name: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!("odev-package-{name}-{nanos}"))
}
#[test]
fn parquet_release_version_matches_manifest() {
let parquet = all_packages()
.into_iter()
.find(|package| package.name() == "integrations/parquet")
.unwrap();
assert_eq!(parquet.version, Version::parse("0.8.0").unwrap());
}
#[test]
fn update_manifest_version_supports_workspace_package_version() {
let mut manifest = DocumentMut::from_str(
r#"
[workspace.package]
version = "0.55.0"
[package]
name = "opendal"
version = { workspace = true }
"#,
)
.unwrap();
let updated = update_manifest_version(
&mut manifest,
&Version::parse("0.56.0").unwrap(),
Path::new("core/Cargo.toml"),
);
assert!(updated);
assert_eq!(
manifest["workspace"]["package"]["version"].as_str(),
Some("0.56.0")
);
assert!(!manifest["package"]["version"].is_str());
}
#[test]
fn update_dependency_version_only_touches_release_managed_core_constraints() {
let dir = temp_test_dir("dependency-sync");
std::fs::create_dir_all(&dir).unwrap();
let manifest_path = dir.join("Cargo.toml");
std::fs::write(
&manifest_path,
r#"[package]
name = "demo"
version = "0.8.0"
[dependencies]
opendal = { version = "0.55.0", path = "../../core" }
serde = "1"
[dev-dependencies]
opendal = { version = "0.55.0", path = "../../core", features = ["services-memory"] }
[build-dependencies]
opendal = { version = ">=0", path = "../../core" }
[target.'cfg(unix)'.dependencies]
opendal = { version = "0.55.0", path = "../../core" }
[target.'cfg(windows)'.dependencies]
opendal = { version = "0.55.0", path = "../core" }
"#,
)
.unwrap();
let dependency = Package {
name: "core".to_string(),
path: dir.join("../../core"),
version: Version::parse("0.56.0").unwrap(),
dependencies: vec![],
};
let updated = update_cargo_version(
dir.as_path(),
&Version::parse("0.8.0").unwrap(),
&[dependency],
);
assert!(updated);
let manifest = std::fs::read_to_string(&manifest_path).unwrap();
let manifest = DocumentMut::from_str(&manifest).unwrap();
assert_eq!(
manifest["dependencies"]["opendal"]["version"].as_str(),
Some("0.56.0")
);
assert_eq!(
manifest["dev-dependencies"]["opendal"]["version"].as_str(),
Some("0.56.0")
);
assert_eq!(
manifest["target"]["cfg(unix)"]["dependencies"]["opendal"]["version"].as_str(),
Some("0.56.0")
);
assert_eq!(
manifest["build-dependencies"]["opendal"]["version"].as_str(),
Some(">=0")
);
assert_eq!(
manifest["target"]["cfg(windows)"]["dependencies"]["opendal"]["version"].as_str(),
Some("0.55.0")
);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn sync_workspace_internal_dependency_versions_updates_workspace_path_dependencies() {
let dir = temp_test_dir("workspace-sync");
let crate_dir = dir.join("crate-a");
let dep_dir = dir.join("dep-b");
std::fs::create_dir_all(&crate_dir).unwrap();
std::fs::create_dir_all(&dep_dir).unwrap();
std::fs::write(
dir.join("Cargo.toml"),
r#"[workspace]
members = ["crate-a", "dep-b"]
[workspace.package]
version = "0.56.0"
"#,
)
.unwrap();
std::fs::write(
crate_dir.join("Cargo.toml"),
r#"[package]
name = "crate-a"
version = "0.56.0"
[dependencies]
dep-b = { path = "../dep-b", version = "0.55.0" }
serde = "1"
"#,
)
.unwrap();
std::fs::write(
dep_dir.join("Cargo.toml"),
r#"[package]
name = "dep-b"
version = "0.56.0"
"#,
)
.unwrap();
let updated = sync_workspace_internal_dependency_versions(
dir.as_path(),
&Version::parse("0.56.0").unwrap(),
);
assert!(updated);
let manifest = std::fs::read_to_string(crate_dir.join("Cargo.toml")).unwrap();
let manifest = DocumentMut::from_str(&manifest).unwrap();
assert_eq!(
manifest["dependencies"]["dep-b"]["version"].as_str(),
Some("0.56.0")
);
assert_eq!(manifest["dependencies"]["serde"].as_str(), Some("1"));
let dep_manifest = std::fs::read_to_string(dep_dir.join("Cargo.toml")).unwrap();
let dep_manifest = DocumentMut::from_str(&dep_manifest).unwrap();
assert_eq!(dep_manifest["package"]["version"].as_str(), Some("0.56.0"));
std::fs::remove_dir_all(dir).unwrap();
}
}