Skip to content

Commit 32f4d88

Browse files
authored
feat: package record (#1148)
1 parent 73f0d35 commit 32f4d88

30 files changed

+1327
-273
lines changed

crates/rattler_conda_types/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub use match_spec::{
4242
parse::ParseMatchSpecError,
4343
MatchSpec, Matches, NamelessMatchSpec,
4444
};
45-
pub use no_arch_type::{NoArchKind, NoArchType};
45+
pub use no_arch_type::{NoArchKind, NoArchType, RawNoArchType};
4646
pub use package_name::{InvalidPackageNameError, PackageName};
4747
pub use parse_mode::ParseStrictness;
4848
pub use platform::{Arch, ParseArchError, ParsePlatformError, Platform};

crates/rattler_conda_types/src/no_arch_type.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
77
/// the [`NoArchType`] and [`NoArchKind`] for a higher level API.
88
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
99
pub enum RawNoArchType {
10-
/// A generic noarch package. This differs from [`GenericV2`] by how it is stored in the
10+
/// A generic noarch package. This differs from `GenericV2` by how it is stored in the
1111
/// repodata (old-format vs new-format)
1212
GenericV1,
1313

14-
/// A generic noarch package. This differs from [`GenericV1`] by how it is stored in the
14+
/// A generic noarch package. This differs from `GenericV1` by how it is stored in the
1515
/// repodata (old-format vs new-format)
1616
GenericV2,
1717

js-rattler/Cargo.lock

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js-rattler/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ wee_alloc = { version = "0.4.5", optional = true }
3535
thiserror = "2.0.12"
3636

3737
rattler_conda_types = { path = "../crates/rattler_conda_types" }
38+
rattler_digest = { path = "../crates/rattler_digest" }
3839
rattler_repodata_gateway = { path = "../crates/rattler_repodata_gateway", features = ["gateway"] }
3940
rattler_solve = { path = "../crates/rattler_solve", default-features = false, features = ["resolvo"] }
4041

@@ -45,6 +46,8 @@ url = "2.5.4"
4546
# compatible with wasm.
4647
bzip2 = { version = "0.5.1", features = ["libbz2-rs-sys"] }
4748

49+
chrono = { version = "0.4.40", features = ["wasmbind"] }
50+
4851
[dev-dependencies]
4952
wasm-bindgen-test = "0.3.45"
5053

js-rattler/crate/error.rs

+5-10
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,19 @@ pub enum JsError {
3232
Serde(#[from] serde_wasm_bindgen::Error),
3333
#[error(transparent)]
3434
PackageNameError(#[from] InvalidPackageNameError),
35+
#[error("{0} is not a valid hex encoded MD5 hash")]
36+
InvalidHexMd5(String),
37+
#[error("{0} is not a valid hex encoded SHA256 hash")]
38+
InvalidHexSha256(String),
3539
}
3640

3741
pub type JsResult<T> = Result<T, JsError>;
3842

3943
impl From<JsError> for JsValue {
4044
fn from(error: JsError) -> Self {
4145
match error {
42-
JsError::InvalidVersion(error) => JsValue::from_str(&error.to_string()),
43-
JsError::VersionExtendError(error) => JsValue::from_str(&error.to_string()),
44-
JsError::VersionBumpError(error) => JsValue::from_str(&error.to_string()),
45-
JsError::ParseVersionSpecError(error) => JsValue::from_str(&error.to_string()),
46-
JsError::ParseChannel(error) => JsValue::from_str(&error.to_string()),
47-
JsError::ParsePlatform(error) => JsValue::from_str(&error.to_string()),
48-
JsError::ParseMatchSpec(error) => JsValue::from_str(&error.to_string()),
49-
JsError::GatewayError(error) => JsValue::from_str(&error.to_string()),
50-
JsError::SolveError(error) => JsValue::from_str(&error.to_string()),
51-
JsError::PackageNameError(error) => JsValue::from_str(&error.to_string()),
5246
JsError::Serde(error) => error.into(),
47+
error => JsValue::from_str(&error.to_string()),
5348
}
5449
}
5550
}

js-rattler/crate/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
mod error;
22
mod gateway;
3+
mod noarch_type;
4+
mod package_name;
5+
mod package_record;
36
mod parse_strictness;
7+
mod platform;
48
pub mod solve;
59
mod utils;
610
mod version;
711
mod version_spec;
12+
mod version_with_source;
813

914
pub use error::{JsError, JsResult};
1015

js-rattler/crate/noarch_type.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Noarch packages are packages that are not architecture specific and therefore
3+
* only have to be built once. A `NoArchType` is either specific to an
4+
* architecture or not.
5+
*
6+
* @public
7+
*/
8+
export declare type NoArchType = undefined | true | "python" | "generic";

js-rattler/crate/noarch_type.rs

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use rattler_conda_types::{NoArchType, RawNoArchType};
2+
use serde::de::Error;
3+
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
4+
use wasm_bindgen_futures::{js_sys, js_sys::JsString};
5+
6+
#[wasm_bindgen(typescript_custom_section)]
7+
const NOARCH_TYPE_D_TS: &'static str = include_str!("noarch_type.d.ts");
8+
9+
#[wasm_bindgen]
10+
#[rustfmt::skip] // Otherwise rustfmt stips the literals
11+
extern "C" {
12+
#[wasm_bindgen(typescript_type = "NoArchType")]
13+
pub type JsNoArchType;
14+
15+
#[wasm_bindgen(thread_local_v2, static_string)]
16+
static GENERIC: JsString = "generic";
17+
18+
#[wasm_bindgen(thread_local_v2, static_string)]
19+
static PYTHON: JsString = "python";
20+
}
21+
22+
impl From<NoArchType> for JsNoArchType {
23+
fn from(value: NoArchType) -> Self {
24+
let value = match value.0 {
25+
None => JsValue::UNDEFINED,
26+
Some(RawNoArchType::GenericV1) => JsValue::FALSE,
27+
Some(RawNoArchType::GenericV2) => GENERIC.with(|str| str.into()),
28+
Some(RawNoArchType::Python) => PYTHON.with(|str| str.into()),
29+
};
30+
JsNoArchType::from(value)
31+
}
32+
}
33+
34+
impl TryFrom<JsNoArchType> for NoArchType {
35+
type Error = serde_wasm_bindgen::Error;
36+
37+
fn try_from(value: JsNoArchType) -> Result<Self, Self::Error> {
38+
if let Some(str) = value.obj.as_string() {
39+
if str == "generic" {
40+
return Ok(NoArchType(Some(RawNoArchType::GenericV2)));
41+
} else if str == "python" {
42+
return Ok(NoArchType(Some(RawNoArchType::Python)));
43+
}
44+
} else if value.obj.is_truthy() {
45+
return Ok(NoArchType(Some(RawNoArchType::GenericV1)));
46+
} else if value.obj.is_falsy() {
47+
return Ok(NoArchType(None));
48+
}
49+
50+
Err(serde_wasm_bindgen::Error::custom("Invalid NoArchType"))
51+
}
52+
}

js-rattler/crate/package_name.d.ts

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/** A unique symbol used for branding `PackageName` types. */
2+
declare const PACKAGE_NAME_BRAND: unique symbol;
3+
4+
/**
5+
* A **branded type** representing a validated package name.
6+
*
7+
* - This type is **enforced at runtime** using `isPackageName()`.
8+
* - Ensures that a package name conforms to the expected format.
9+
*
10+
* @example
11+
*
12+
* ```ts
13+
* const pkg: PackageName = "valid-package" as PackageName;
14+
* ```
15+
*
16+
* @public
17+
*/
18+
export declare type PackageName = string & {
19+
[PACKAGE_NAME_BRAND]: void;
20+
};
21+
22+
/**
23+
* Defines the allowed characters for any package name.
24+
*
25+
* Allowed characters:
26+
*
27+
* - Lowercase letters (`a-z`)
28+
* - Uppercase letters (`A-Z`)
29+
* - Digits (`0-9`)
30+
* - Underscore (`_`)
31+
* - Dash (`-`)
32+
* - Dot (`.`)
33+
*
34+
* @public
35+
*/
36+
export declare type PackageNameChar =
37+
| "a"
38+
| "b"
39+
| "c"
40+
| "d"
41+
| "e"
42+
| "f"
43+
| "g"
44+
| "h"
45+
| "i"
46+
| "j"
47+
| "k"
48+
| "l"
49+
| "m"
50+
| "n"
51+
| "o"
52+
| "p"
53+
| "q"
54+
| "r"
55+
| "s"
56+
| "t"
57+
| "u"
58+
| "v"
59+
| "w"
60+
| "x"
61+
| "y"
62+
| "z"
63+
| "A"
64+
| "B"
65+
| "C"
66+
| "D"
67+
| "E"
68+
| "F"
69+
| "G"
70+
| "H"
71+
| "I"
72+
| "J"
73+
| "K"
74+
| "L"
75+
| "M"
76+
| "N"
77+
| "O"
78+
| "P"
79+
| "Q"
80+
| "R"
81+
| "S"
82+
| "T"
83+
| "U"
84+
| "V"
85+
| "W"
86+
| "X"
87+
| "Y"
88+
| "Z"
89+
| "0"
90+
| "1"
91+
| "2"
92+
| "3"
93+
| "4"
94+
| "5"
95+
| "6"
96+
| "7"
97+
| "8"
98+
| "9"
99+
| "_"
100+
| "-"
101+
| ".";
102+
103+
/**
104+
* Ensures that a string is a valid package name.
105+
*
106+
* - If `S` contains only valid characters and is not empty, it resolves to `S`.
107+
* - Otherwise, it resolves to `never`.
108+
*
109+
* @example
110+
*
111+
* ```ts
112+
* type Valid = PackageNameLiteral<"valid-name">; // 'valid-name'
113+
* type Invalid = PackageNameLiteral<"invalid!">; // never
114+
* type Empty = PackageNameLiteral<"">; // never
115+
* ```
116+
*
117+
* @public
118+
*/
119+
export declare type PackageNameLiteral<S extends string> =
120+
ContainsOnlyPackageNameChars<S> & NonEmptyString<S>;
121+
122+
/**
123+
* A type that accepts:
124+
*
125+
* - A **PackageName** (a runtime-validated string).
126+
* - A **string literal** that satisfies `PackageNameLiteral<S>`.
127+
*
128+
* This is useful for functions that accept both validated runtime values and
129+
* compile-time checked literals.
130+
*
131+
* @example
132+
*
133+
* ```ts
134+
* function processPackage(name: PackageNameOrLiteral) { ... }
135+
*
136+
* processPackage("valid-package"); // ✅ Allowed (checked at compile-time)
137+
* processPackage("invalid!"); // ❌ Compile-time error
138+
* ```
139+
*
140+
* @param S - The input string type.
141+
* @public
142+
*/
143+
export declare type PackageNameOrLiteral<S extends string = string> =
144+
| PackageName
145+
| (S extends PackageNameLiteral<S> ? S : never);
146+
147+
/**
148+
* Checks whether a string consists only of valid package name characters.
149+
*
150+
* - If `S` contains only allowed characters, it resolves to `S`.
151+
* - Otherwise, it resolves to `never`.
152+
*
153+
* @example
154+
*
155+
* ```ts
156+
* type Valid = ContainsOnlyPackageNameChars<"valid-name">; // 'valid-name'
157+
* type Invalid = ContainsOnlyPackageNameChars<"invalid!">; // never
158+
* ```
159+
*
160+
* @public
161+
*/
162+
export declare type ContainsOnlyPackageNameChars<S extends string> =
163+
S extends ""
164+
? ""
165+
: S extends `${infer First}${infer Rest}`
166+
? First extends PackageNameChar
167+
? ContainsOnlyPackageNameChars<Rest> extends never
168+
? never
169+
: S
170+
: never
171+
: never;
172+
173+
/**
174+
* Ensures that a string is non-empty.
175+
*
176+
* - If `T` is an empty string, it resolves to `never`.
177+
* - Otherwise, it resolves to `T`.
178+
*
179+
* @example
180+
*
181+
* ```ts
182+
* type Valid = NonEmptyString<"hello">; // 'hello'
183+
* type Invalid = NonEmptyString<"">; // never
184+
* ```
185+
*
186+
* @public
187+
*/
188+
export declare type NonEmptyString<T extends string> = T extends "" ? never : T;

js-rattler/crate/package_name.rs

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use wasm_bindgen::prelude::wasm_bindgen;
2+
3+
#[wasm_bindgen(typescript_custom_section)]
4+
const PACKAGE_NAME_D_TS: &'static str = include_str!("package_name.d.ts");
5+
6+
#[wasm_bindgen]
7+
extern "C" {
8+
#[wasm_bindgen(typescript_type = "PackageName")]
9+
pub type JsPackageName;
10+
}

js-rattler/crate/package_record.d.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* A package record is a JSON object that contains metadata about a package.
3+
*
4+
* @public
5+
*/
6+
export declare type PackageRecordJson = {
7+
name: string;
8+
version: string;
9+
build: string;
10+
build_number: number;
11+
subdir: Platform;
12+
arch?: Arch;
13+
constrains?: string[];
14+
depends?: string[];
15+
features?: string;
16+
license?: string;
17+
license_family?: string;
18+
md5?: string;
19+
legacy_bz2_md5?: string;
20+
legacy_bz2_size?: number;
21+
sha256?: string;
22+
platform?: string;
23+
python_site_packages_path?: string;
24+
size?: number;
25+
noarch?: NoArchType;
26+
timestamp?: number;
27+
track_features?: string;
28+
};

0 commit comments

Comments
 (0)