fix: System skills marker includes nested folders recursively (#10350)

Updated system skills bundled with Codex were not correctly replacing
the user's skills in their .system folder.

- Fix `.codex-system-skills.marker` not updating by hashing embedded
system skills recursively (nested dirs + file contents), so updates
trigger a reinstall.
- Added a build Cargo hook to rerun if there are changes in
`src/skills/assets/samples/*`, ensuring embedded skill updates rebuild
correctly under caching.
- Add a small unit test to ensure nested entries are included in the
fingerprint.
This commit is contained in:
Gav Verma
2026-02-01 18:17:32 -08:00
committed by pash
parent d17ac501eb
commit 5a32ef8cd5
3 changed files with 74 additions and 15 deletions

View File

@@ -3,6 +3,7 @@ edition.workspace = true
license.workspace = true
name = "codex-core"
version.workspace = true
build = "build.rs"
[lib]
doctest = false

27
codex-rs/core/build.rs Normal file
View File

@@ -0,0 +1,27 @@
use std::fs;
use std::path::Path;
fn main() {
let samples_dir = Path::new("src/skills/assets/samples");
if !samples_dir.exists() {
return;
}
println!("cargo:rerun-if-changed={}", samples_dir.display());
visit_dir(samples_dir);
}
fn visit_dir(dir: &Path) {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
println!("cargo:rerun-if-changed={}", path.display());
if path.is_dir() {
visit_dir(&path);
}
}
}

View File

@@ -86,21 +86,8 @@ fn read_marker(path: &AbsolutePathBuf) -> Result<String, SystemSkillsError> {
}
fn embedded_system_skills_fingerprint() -> String {
let mut items: Vec<(String, Option<u64>)> = SYSTEM_SKILLS_DIR
.entries()
.iter()
.map(|entry| match entry {
include_dir::DirEntry::Dir(dir) => (dir.path().to_string_lossy().to_string(), None),
include_dir::DirEntry::File(file) => {
let mut file_hasher = DefaultHasher::new();
file.contents().hash(&mut file_hasher);
(
file.path().to_string_lossy().to_string(),
Some(file_hasher.finish()),
)
}
})
.collect();
let mut items = Vec::new();
collect_fingerprint_items(&SYSTEM_SKILLS_DIR, &mut items);
items.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
let mut hasher = DefaultHasher::new();
@@ -112,6 +99,25 @@ fn embedded_system_skills_fingerprint() -> String {
format!("{:x}", hasher.finish())
}
fn collect_fingerprint_items(dir: &Dir<'_>, items: &mut Vec<(String, Option<u64>)>) {
for entry in dir.entries() {
match entry {
include_dir::DirEntry::Dir(subdir) => {
items.push((subdir.path().to_string_lossy().to_string(), None));
collect_fingerprint_items(subdir, items);
}
include_dir::DirEntry::File(file) => {
let mut file_hasher = DefaultHasher::new();
file.contents().hash(&mut file_hasher);
items.push((
file.path().to_string_lossy().to_string(),
Some(file_hasher.finish()),
));
}
}
}
}
/// Writes the embedded `include_dir::Dir` to disk under `dest`.
///
/// Preserves the embedded directory structure.
@@ -163,3 +169,28 @@ impl SystemSkillsError {
Self::Io { action, source }
}
}
#[cfg(test)]
mod tests {
use super::SYSTEM_SKILLS_DIR;
use super::collect_fingerprint_items;
#[test]
fn fingerprint_traverses_nested_entries() {
let mut items = Vec::new();
collect_fingerprint_items(&SYSTEM_SKILLS_DIR, &mut items);
let mut paths: Vec<String> = items.into_iter().map(|(path, _)| path).collect();
paths.sort_unstable();
assert!(
paths
.binary_search_by(|probe| probe.as_str().cmp("skill-creator/SKILL.md"))
.is_ok()
);
assert!(
paths
.binary_search_by(|probe| probe.as_str().cmp("skill-creator/scripts/init_skill.py"))
.is_ok()
);
}
}