OXYGENHUB

flow-casfs: a read-only FUSE mount of an Oxygen state (refs #21) f849c7889471

The tree from the repository's state index; an opaque file's bytes from
its blob; a text file's from its own text blob when it was added once and
never edited (read with its directory's other such files in one batch),
else rendered from its history and cached on disk by graph key. Measured
on the Linux tip on a loaded Earth: mount 0.5 to 0.9 s, first read
0.1 s, a cold full read 52 s, a warm one 0.65 s, identical to the working
copy under diff -r. Not yet: several FUSE threads, a bound on the bytes
kept in memory, read-ahead, a lazy state index for a mount under 100 ms.

fuser 0.16 without libfuse (fusermount3); the build image is rebuilt
with it vendored and pinned here and in oxygen and oxygenhub; the oxygen
pin moves to main for State::single_text and Repo::single_texts.
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-01 02:12 UTC
Signature
✔ signed by Andy McCall
Hash
f849c7889471a035af5b44825022b68168df394a3944521071ae7b0feadf1aed
Depends on
5c4016b9c5eb 728b940f2c5f e0918b887850 f29b88af0d9b fe128fa67a06
Depended on by
e3fb0e9b5e6b 412808f9f769 48ed8a1db369 5544c8998aa7 9296a15494d3 cce63af9ec54 6f66e397539c 28cc603f737d 1408324d5d55 6a2d31568cf2 da72c3b192c3 ecce3719da80 59078d2b8d46 85ad9087a566 e611ad52a136 602ac9c8ed1d
In bookmarks
main newer-pipeline push-uses-login
In tags
v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.5.0 v0.5.1 v0.5.2 v0.5.3
Edits
17 atoms, 4 tree operations, 6 files

Browse the tree at this change

added crates/flow-casfs/Cargo.toml +20 −0
1 +
[package]
2 +
name = "flow-casfs"
3 +
version.workspace = true
4 +
edition.workspace = true
5 +
license.workspace = true
6 +
description = "A read-only FUSE mount of an Oxygen state (OxygenFlow spec 5.1)"
7 +
8 +
[[bin]]
9 +
name = "flow-casfs"
10 +
path = "src/main.rs"
11 +
12 +
[dependencies]
13 +
oxy-core.workspace = true
14 +
anyhow.workspace = true
15 +
# Pure Rust: mounts through the fusermount3 binary, no libfuse headers.
16 +
fuser = { version = "0.16", default-features = false }
17 +
libc = "0.2"
18 +
19 +
[dev-dependencies]
20 +
tempfile.workspace = true
added crates/flow-casfs/src/main.rs +347 −0
1 +
//! flow-casfs: an Oxygen state mounted read-only (OxygenFlow spec 5.1).
2 +
//!
3 +
//! The tree comes from the repository's state index; an opaque file's bytes
4 +
//! are its stored blob; a text file's bytes are a rendering of its history,
5 +
//! made on first read or stat and cached on disk by graph key (equal keys
6 +
//! mean equal renderings), so every state in which a file has the same
7 +
//! history finds it at once.
8 +
//!
9 +
//!   flow-casfs mount REPO MOUNTPOINT [--bookmark NAME]
10 +
//!
11 +
//! It mounts in the foreground and prints how long the mount took; unmount
12 +
//! with `fusermount3 -u MOUNTPOINT`.
13 +
14 +
use anyhow::{bail, Context, Result};
15 +
use fuser::{FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, Request};
16 +
use oxy_core::ids::NodeId;
17 +
use oxy_core::objects::NodeKind;
18 +
use oxy_core::repo::Repo;
19 +
use oxy_core::state::State;
20 +
use std::collections::HashMap;
21 +
use std::ffi::{OsStr, OsString};
22 +
use std::path::{Path, PathBuf};
23 +
use std::sync::Arc;
24 +
use std::time::{Duration, Instant, SystemTime};
25 +
26 +
/// How long the kernel may keep attributes and names: the mount never
27 +
/// changes.
28 +
const TTL: Duration = Duration::from_secs(3600);
29 +
30 +
struct Inode {
31 +
    parent: u64,
32 +
    kind: NodeKind,
33 +
    node: NodeId,
34 +
    exec: bool,
35 +
    opaque: bool,
36 +
    /// For a directory: its children by name.
37 +
    children: Vec<(OsString, u64)>,
38 +
}
39 +
40 +
struct CasFs {
41 +
    repo: Repo,
42 +
    state: State,
43 +
    inodes: Vec<Inode>,
44 +
    /// Bytes already read, by inode.
45 +
    bytes: HashMap<u64, Arc<Vec<u8>>>,
46 +
    cache_dir: PathBuf,
47 +
    uid: u32,
48 +
    gid: u32,
49 +
    mounted: SystemTime,
50 +
}
51 +
52 +
impl CasFs {
53 +
    fn new(repo: Repo, state: State) -> CasFs {
54 +
        let view = state.tree().view();
55 +
        let root = Inode { parent: 1, kind: NodeKind::Dir, node: NodeId::ROOT, exec: false, opaque: false, children: Vec::new() };
56 +
        let mut inodes = vec![root];
57 +
        let mut by_path: HashMap<&str, u64> = HashMap::with_capacity(view.entries.len());
58 +
        by_path.insert("", 1);
59 +
        // In path order, so a directory comes before what it holds.
60 +
        for (path, e) in view.entries.iter() {
61 +
            let (dir, name) = path.rsplit_once('/').unwrap_or(("", path.as_str()));
62 +
            let Some(&parent) = by_path.get(dir) else { continue };
63 +
            let ino = inodes.len() as u64 + 1;
64 +
            inodes.push(Inode { parent, kind: e.kind, node: e.node, exec: e.mode.exec, opaque: e.mode.opaque, children: Vec::new() });
65 +
            inodes[parent as usize - 1].children.push((OsString::from(name), ino));
66 +
            if e.kind == NodeKind::Dir || e.kind == NodeKind::Subrepo {
67 +
                by_path.insert(path.as_str(), ino);
68 +
            }
69 +
        }
70 +
        for i in inodes.iter_mut() {
71 +
            i.children.sort();
72 +
        }
73 +
        let cache_dir = repo.root().join(".oxy").join("cache").join("render");
74 +
        CasFs {
75 +
            repo,
76 +
            state,
77 +
            inodes,
78 +
            bytes: HashMap::new(),
79 +
            cache_dir,
80 +
            uid: unsafe { libc::getuid() },
81 +
            gid: unsafe { libc::getgid() },
82 +
            mounted: SystemTime::now(),
83 +
        }
84 +
    }
85 +
86 +
    fn inode(&self, ino: u64) -> Option<&Inode> {
87 +
        self.inodes.get(ino.checked_sub(1)? as usize)
88 +
    }
89 +
90 +
    /// A file's or symlink's bytes: its blob, or its rendering (cached on
91 +
    /// disk by graph key).
92 +
    fn content(&mut self, ino: u64) -> Result<Arc<Vec<u8>>> {
93 +
        if let Some(b) = self.bytes.get(&ino) {
94 +
            return Ok(b.clone());
95 +
        }
96 +
        let i = self.inode(ino).context("no such inode")?;
97 +
        let (node, kind, opaque, parent) = (i.node, i.kind, i.opaque, i.parent);
98 +
        // A file added once and never edited: read it with every such file
99 +
        // of its directory not read yet, in one batch (a directory is
100 +
        // usually read whole), on every core.
101 +
        if kind == NodeKind::File && !opaque && self.state.single_text(&node).is_some() {
102 +
            let siblings: Vec<(u64, oxy_core::objects::TextRef)> = self.inodes[parent as usize - 1]
103 +
                .children
104 +
                .iter()
105 +
                .map(|(_, c)| *c)
106 +
                .filter(|c| !self.bytes.contains_key(c))
107 +
                .filter_map(|c| {
108 +
                    let i = &self.inodes[c as usize - 1];
109 +
                    (i.kind == NodeKind::File && !i.opaque).then_some(())?;
110 +
                    Some((c, self.state.single_text(&i.node)?))
111 +
                })
112 +
                .collect();
113 +
            let texts: Vec<oxy_core::objects::TextRef> = siblings.iter().map(|(_, t)| *t).collect();
114 +
            for ((c, _), b) in siblings.iter().zip(self.repo.single_texts(&texts)?) {
115 +
                self.bytes.insert(*c, Arc::new(b));
116 +
            }
117 +
            if let Some(b) = self.bytes.get(&ino) {
118 +
                return Ok(b.clone());
119 +
            }
120 +
        }
121 +
        let bytes = if kind == NodeKind::File && !opaque {
122 +
            self.rendering(&node)?
123 +
        } else {
124 +
            match self.state.live_blobs(&node).first() {
125 +
                Some(b) => self.repo.read_blob(b)?,
126 +
                None => Vec::new(),
127 +
            }
128 +
        };
129 +
        let bytes = Arc::new(bytes);
130 +
        self.bytes.insert(ino, bytes.clone());
131 +
        Ok(bytes)
132 +
    }
133 +
134 +
    fn rendering(&self, node: &NodeId) -> Result<Vec<u8>> {
135 +
        // A file added once and never edited is its own text: no graph, and
136 +
        // nothing to cache.
137 +
        if let Some(t) = self.state.single_text(node) {
138 +
            return Ok(self.repo.single_texts(&[t])?.pop().unwrap_or_default());
139 +
        }
140 +
        let key = self.state.graph_key(node);
141 +
        let cached = key.map(|k| self.cache_dir.join(hex(&k)));
142 +
        if let Some(p) = &cached {
143 +
            if let Ok(b) = std::fs::read(p) {
144 +
                return Ok(b);
145 +
            }
146 +
        }
147 +
        let graphs = self.repo.build_graphs_detached(&self.state, &[*node])?;
148 +
        let bytes = match graphs.get(node) {
149 +
            Some(g) => self.state.render_graph(g).0,
150 +
            None => Vec::new(),
151 +
        };
152 +
        if let Some(p) = cached {
153 +
            // A cache: written whole or not at all, without waiting for
154 +
            // the disk.
155 +
            let _ = std::fs::create_dir_all(&self.cache_dir);
156 +
            let tmp = p.with_extension(format!("tmp-{}", std::process::id()));
157 +
            if std::fs::write(&tmp, &bytes).is_ok() {
158 +
                let _ = std::fs::rename(&tmp, &p);
159 +
            }
160 +
        }
161 +
        Ok(bytes)
162 +
    }
163 +
164 +
    fn attr(&mut self, ino: u64) -> Result<FileAttr> {
165 +
        let i = self.inode(ino).context("no such inode")?;
166 +
        let (kind, perm) = match i.kind {
167 +
            NodeKind::Dir | NodeKind::Subrepo => (FileType::Directory, 0o555),
168 +
            NodeKind::Symlink => (FileType::Symlink, 0o777),
169 +
            _ if i.exec => (FileType::RegularFile, 0o555),
170 +
            _ => (FileType::RegularFile, 0o444),
171 +
        };
172 +
        let nlink = if kind == FileType::Directory { 2 } else { 1 };
173 +
        let size = match kind {
174 +
            FileType::Directory => 0,
175 +
            _ => self.content(ino)?.len() as u64,
176 +
        };
177 +
        Ok(FileAttr {
178 +
            ino,
179 +
            size,
180 +
            blocks: size.div_ceil(512),
181 +
            atime: self.mounted,
182 +
            mtime: self.mounted,
183 +
            ctime: self.mounted,
184 +
            crtime: self.mounted,
185 +
            kind,
186 +
            perm,
187 +
            nlink,
188 +
            uid: self.uid,
189 +
            gid: self.gid,
190 +
            rdev: 0,
191 +
            blksize: 4096,
192 +
            flags: 0,
193 +
        })
194 +
    }
195 +
}
196 +
197 +
fn hex(b: &[u8]) -> String {
198 +
    b.iter().map(|x| format!("{x:02x}")).collect()
199 +
}
200 +
201 +
impl Filesystem for CasFs {
202 +
    fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) {
203 +
        let found =
204 +
            self.inode(parent).and_then(|p| p.children.binary_search_by(|(n, _)| n.as_os_str().cmp(name)).ok().map(|i| p.children[i].1));
205 +
        match found.map(|ino| self.attr(ino)) {
206 +
            Some(Ok(a)) => reply.entry(&TTL, &a, 0),
207 +
            Some(Err(_)) => reply.error(libc::EIO),
208 +
            None => reply.error(libc::ENOENT),
209 +
        }
210 +
    }
211 +
212 +
    fn getattr(&mut self, _req: &Request<'_>, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
213 +
        match self.attr(ino) {
214 +
            Ok(a) => reply.attr(&TTL, &a),
215 +
            Err(_) => reply.error(libc::ENOENT),
216 +
        }
217 +
    }
218 +
219 +
    fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyData) {
220 +
        match self.content(ino) {
221 +
            Ok(b) => reply.data(&b),
222 +
            Err(_) => reply.error(libc::EIO),
223 +
        }
224 +
    }
225 +
226 +
    #[allow(clippy::too_many_arguments)]
227 +
    fn read(&mut self, _req: &Request<'_>, ino: u64, _fh: u64, offset: i64, size: u32, _flags: i32, _lock: Option<u64>, reply: ReplyData) {
228 +
        match self.content(ino) {
229 +
            Ok(b) => {
230 +
                let start = (offset.max(0) as usize).min(b.len());
231 +
                let end = (start + size as usize).min(b.len());
232 +
                reply.data(&b[start..end]);
233 +
            }
234 +
            Err(_) => reply.error(libc::EIO),
235 +
        }
236 +
    }
237 +
238 +
    fn readdir(&mut self, _req: &Request<'_>, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
239 +
        let Some(dir) = self.inode(ino) else {
240 +
            reply.error(libc::ENOENT);
241 +
            return;
242 +
        };
243 +
        let mut entries: Vec<(u64, FileType, OsString)> =
244 +
            vec![(ino, FileType::Directory, ".".into()), (dir.parent, FileType::Directory, "..".into())];
245 +
        for (name, child) in &dir.children {
246 +
            let kind = match self.inodes[*child as usize - 1].kind {
247 +
                NodeKind::Dir | NodeKind::Subrepo => FileType::Directory,
248 +
                NodeKind::Symlink => FileType::Symlink,
249 +
                _ => FileType::RegularFile,
250 +
            };
251 +
            entries.push((*child, kind, name.clone()));
252 +
        }
253 +
        for (i, (child, kind, name)) in entries.into_iter().enumerate().skip(offset.max(0) as usize) {
254 +
            if reply.add(child, (i + 1) as i64, kind, name) {
255 +
                break;
256 +
            }
257 +
        }
258 +
        reply.ok();
259 +
    }
260 +
}
261 +
262 +
fn main() -> Result<()> {
263 +
    let args: Vec<String> = std::env::args().collect();
264 +
    let usage = "usage: flow-casfs mount REPO MOUNTPOINT [--bookmark NAME]";
265 +
    if args.len() < 4 || args[1] != "mount" {
266 +
        bail!("{usage}");
267 +
    }
268 +
    let (repo_dir, mountpoint) = (Path::new(&args[2]), Path::new(&args[3]));
269 +
    let bookmark = args.iter().position(|a| a == "--bookmark").and_then(|i| args.get(i + 1)).cloned();
270 +
    let started = Instant::now();
271 +
    let mut repo = Repo::open(repo_dir).with_context(|| format!("open {}", repo_dir.display()))?;
272 +
    if let Some(b) = &bookmark {
273 +
        repo.config.bookmark = b.clone();
274 +
    }
275 +
    let heads = repo.bookmark_heads()?;
276 +
    let state = repo.load_state_indexed(&heads)?;
277 +
    let loaded = started.elapsed();
278 +
    let fs = CasFs::new(repo, state);
279 +
    eprintln!(
280 +
        "flow-casfs: {} entries; state loaded in {:.1} ms, tree built in {:.1} ms",
281 +
        fs.inodes.len(),
282 +
        loaded.as_secs_f64() * 1000.0,
283 +
        (started.elapsed() - loaded).as_secs_f64() * 1000.0
284 +
    );
285 +
    let options = [MountOption::RO, MountOption::FSName("flow-casfs".into()), MountOption::DefaultPermissions];
286 +
    let session = fuser::spawn_mount2(fs, mountpoint, &options).context("mount")?;
287 +
    eprintln!("flow-casfs: mounted at {} in {:.1} ms", mountpoint.display(), started.elapsed().as_secs_f64() * 1000.0);
288 +
    // Until the mount goes away (fusermount3 -u) or the process is stopped.
289 +
    session.guard.join().ok();
290 +
    Ok(())
291 +
}
292 +
293 +
#[cfg(test)]
294 +
mod tests {
295 +
    use super::*;
296 +
297 +
    /// The inode of a path, walking from the root.
298 +
    fn ino_of(fs: &CasFs, path: &str) -> Option<u64> {
299 +
        let mut ino = 1u64;
300 +
        for part in path.split('/') {
301 +
            let dir = fs.inode(ino)?;
302 +
            let i = dir.children.binary_search_by(|(n, _)| n.as_os_str().cmp(OsStr::new(part))).ok()?;
303 +
            ino = dir.children[i].1;
304 +
        }
305 +
        Some(ino)
306 +
    }
307 +
308 +
    #[test]
309 +
    fn a_mounted_state_holds_the_committed_files() {
310 +
        let dir = tempfile::tempdir().unwrap();
311 +
        std::env::set_var("OXY_HOME", dir.path().join("home"));
312 +
        let root = dir.path().join("repo");
313 +
        let repo = Repo::init(&root).unwrap();
314 +
        let big: String = (0..3000).map(|i| format!("line {i}\n")).collect();
315 +
        let files = [("a.txt", "one\ntwo\n".to_string()), ("src/lib.rs", "pub fn f() {}\n".to_string()), ("src/deep/big.txt", big)];
316 +
        for (p, c) in &files {
317 +
            let full = root.join(p);
318 +
            std::fs::create_dir_all(full.parent().unwrap()).unwrap();
319 +
            std::fs::write(full, c).unwrap();
320 +
        }
321 +
        repo.commit("first", &[]).unwrap();
322 +
        // An edit, so one file's rendering is a history, not one text.
323 +
        std::fs::write(root.join("a.txt"), "one\n2\ntwo\n").unwrap();
324 +
        repo.commit("second", &[]).unwrap();
325 +
        let heads = repo.bookmark_heads().unwrap();
326 +
        let state = repo.load_state_indexed(&heads).unwrap();
327 +
        let mut fs = CasFs::new(Repo::open(&root).unwrap(), state);
328 +
        for (p, c) in [("a.txt", "one\n2\ntwo\n"), ("src/lib.rs", "pub fn f() {}\n")] {
329 +
            let ino = ino_of(&fs, p).unwrap();
330 +
            assert_eq!(fs.content(ino).unwrap().as_slice(), c.as_bytes(), "{p}");
331 +
        }
332 +
        let big = ino_of(&fs, "src/deep/big.txt").unwrap();
333 +
        assert_eq!(fs.content(big).unwrap().as_slice(), files[2].1.as_bytes());
334 +
        assert_eq!(fs.attr(big).unwrap().size, files[2].1.len() as u64);
335 +
        assert_eq!(fs.attr(ino_of(&fs, "src/deep").unwrap()).unwrap().kind, FileType::Directory);
336 +
        assert!(ino_of(&fs, "missing.txt").is_none());
337 +
        // Only the edited file needed rendering, and its rendering is cached
338 +
        // by graph key, so a new mount reads it back; the others are their
339 +
        // own text blobs.
340 +
        let cached = std::fs::read_dir(&fs.cache_dir).unwrap().count();
341 +
        assert_eq!(cached, 1, "renderings cached");
342 +
        let state = repo.load_state_indexed(&heads).unwrap();
343 +
        let mut again = CasFs::new(Repo::open(&root).unwrap(), state);
344 +
        let ino = ino_of(&again, "a.txt").unwrap();
345 +
        assert_eq!(again.content(ino).unwrap().as_slice(), b"one\n2\ntwo\n");
346 +
    }
347 +
}
modified .flow/pipeline.kdl +3 −3
…
36 unchanged lines
37 37
// vendored crates. oxygen comes from the pinned subrepo in deps/.
38 38
sandbox "rust" {
39 39
    runtime "microvm"
40 -
    rootfs "cas://blob/04be5ef091a4cd31d0e385b9b3c22be20cf19b9386496d3c53a4a711f66f5f1c"
40 +
    rootfs "cas://blob/a3b49f36a2ad1838219ea0b7985591e44ac0e6d7f92e9d993d3a3738263fe32e"
41 41
    memory-limit "8GiB"
42 42
    cpu-limit 4
43 43
    network "loopback"
…
1 unchanged lines
45 45
46 46
sandbox "rust-small" {
47 47
    runtime "microvm"
48 -
    rootfs "cas://blob/04be5ef091a4cd31d0e385b9b3c22be20cf19b9386496d3c53a4a711f66f5f1c"
48 +
    rootfs "cas://blob/a3b49f36a2ad1838219ea0b7985591e44ac0e6d7f92e9d993d3a3738263fe32e"
49 49
    memory-limit "1GiB"
50 50
    cpu-limit 1
51 51
    network "isolated"
…
2 unchanged lines
54 54
job "fmt" {
55 55
    sandbox "rust-small"
56 56
    // This workspace only: --all would reach the pinned oxygen in deps/.
57 -
    exec "cargo" "fmt" "--check" "-p" "flow" "-p" "flow-core" "-p" "flow-init"
57 +
    exec "cargo" "fmt" "--check" "-p" "flow" "-p" "flow-core" "-p" "flow-init" "-p" "flow-casfs"
58 58
}
59 59
60 60
job "clippy" {
…
35 unchanged lines
modified Cargo.lock +76 −0
…
154 unchanged lines
155 155
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
156 156
157 157
[[package]]
158 +
name = "cfg_aliases"
159 +
version = "0.2.2"
160 +
source = "registry+https://github.com/rust-lang/crates.io-index"
161 +
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
162 +
163 +
[[package]]
158 164
name = "clap"
159 165
version = "4.6.7"
160 166
source = "registry+https://github.com/rust-lang/crates.io-index"
…
268 unchanged lines
429 435
 "tempfile",
430 436
 "toml",
431 437
 "ureq",
438 +
]
439 +
440 +
[[package]]
441 +
name = "flow-casfs"
442 +
version = "0.1.0"
443 +
dependencies = [
444 +
 "anyhow",
445 +
 "fuser",
446 +
 "libc",
447 +
 "oxy-core",
448 +
 "tempfile",
432 449
]
433 450
434 451
[[package]]
…
27 unchanged lines
462 479
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
463 480
dependencies = [
464 481
 "percent-encoding",
482 +
]
483 +
484 +
[[package]]
485 +
name = "fuser"
486 +
version = "0.16.0"
487 +
source = "registry+https://github.com/rust-lang/crates.io-index"
488 +
checksum = "0bb29a3ae32279fe3e79a958fe01899f5fb23eadccee919cf88e145b54ed9367"
489 +
dependencies = [
490 +
 "libc",
491 +
 "log",
492 +
 "memchr",
493 +
 "nix",
494 +
 "page_size",
495 +
 "smallvec",
496 +
 "zerocopy",
465 497
]
466 498
467 499
[[package]]
…
321 unchanged lines
789 821
 "ctutils",
790 822
 "hybrid-array",
791 823
 "num-traits",
824 +
]
825 +
826 +
[[package]]
827 +
name = "nix"
828 +
version = "0.29.0"
829 +
source = "registry+https://github.com/rust-lang/crates.io-index"
830 +
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
831 +
dependencies = [
832 +
 "bitflags",
833 +
 "cfg-if",
834 +
 "cfg_aliases",
835 +
 "libc",
792 836
]
793 837
794 838
[[package]]
…
36 unchanged lines
831 875
 "ureq",
832 876
 "webpki-roots 1.0.9",
833 877
 "zstd",
878 +
]
879 +
880 +
[[package]]
881 +
name = "page_size"
882 +
version = "0.6.0"
883 +
source = "registry+https://github.com/rust-lang/crates.io-index"
884 +
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
885 +
dependencies = [
886 +
 "libc",
887 +
 "winapi",
834 888
]
835 889
836 890
[[package]]
…
585 unchanged lines
1422 1476
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
1423 1477
dependencies = [
1424 1478
 "rustls-pki-types",
1479 +
]
1480 +
1481 +
[[package]]
1482 +
name = "winapi"
1483 +
version = "0.3.9"
1484 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1485 +
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1486 +
dependencies = [
1487 +
 "winapi-i686-pc-windows-gnu",
1488 +
 "winapi-x86_64-pc-windows-gnu",
1425 1489
]
1426 1490
1427 1491
[[package]]
1492 +
name = "winapi-i686-pc-windows-gnu"
1493 +
version = "0.4.0"
1494 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1495 +
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1496 +
1497 +
[[package]]
1428 1498
name = "winapi-util"
1429 1499
version = "0.1.11"
1430 1500
source = "registry+https://github.com/rust-lang/crates.io-index"
…
1 unchanged lines
1432 1502
dependencies = [
1433 1503
 "windows-sys 0.61.2",
1434 1504
]
1505 +
1506 +
[[package]]
1507 +
name = "winapi-x86_64-pc-windows-gnu"
1508 +
version = "0.4.0"
1509 +
source = "registry+https://github.com/rust-lang/crates.io-index"
1510 +
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1435 1511
1436 1512
[[package]]
1437 1513
name = "windows-link"
…
243 unchanged lines
modified Cargo.toml +1 −1
1 1
[workspace]
2 -
members = ["crates/flow-core", "crates/flow-init", "crates/flow"]
2 +
members = ["crates/flow-core", "crates/flow-init", "crates/flow", "crates/flow-casfs"]
3 3
# oxygen as a pinned subrepo (oxy submodule): the state flow builds against.
4 4
exclude = ["deps"]
5 5
resolver = "2"
…
19 unchanged lines
replaced deps/oxygen +0 −0

No line changes.