Cargo: exclude all contents of a directory, but keep the directory

[package]
exclude = ["./lammps/potentials/*"]

When the above is used, it not only excludes everything inside ./lammps/potentials, but it also excludes the directory itself. I need to keep an empty directory behind for the C library I am wrapping, whose CMakeLists.txt attempts to copy the directory to PREFIX/usr/share/:

install(DIRECTORY ${LAMMPS_SOURCE_DIR}/../potentials/
        DESTINATION ${LAMMPS_POTENTIALS_DIR})

I can hack it by creating an empty directory in my build script, but build scripts are not supposed to modify the source directory. (normally cargo package catches stuff like this; it just doesn’t seem to notice new directories)

N.B. the directory contains 143 entries and they add more with every release.

If the contents of the directory can be described with some glob, maybe you can create a file like .cargokeep (with name not conforming to this glob) which will simply mark the folder as non-empty and thus not ignored.

The folder is in a submodule. I would have to fork the repository and point the submodule to my own fork just to add a single empty file.

Here’s what the filenames look like. I guess *.* could work as a pattern if I really wanted to do this…


I’m currently considering recursively copying the submodule directory into the output directory just so I can create that empty directory in my build script without any guilt.

Bleeeeeh. The things I do so I can sleep at night.

/// HACK:
/// See https://users.rust-lang.org/t/cargo-exclude-all-contents-of-a-directory-but-keep-the-directory/28137
///
/// Create a dedicated copy of the lammps directory for building.
///
/// The copy will be modified in such a way to ensure that the build succeeds.
///
/// **None of this should be necessary once there is a way to specify in cargo that
/// a directory should be preserved while its contents are ignored.**
pub(crate) fn lammps_repo_dir_build_copy() -> PanicResult<PathDir> {
    let src_dir = lammps_repo_dir();

    let copy = PathDir::create(::env::out_dir().join("lammps"))?;

    let suffix = |entry: &walkdir::DirEntry| {
        entry.path().strip_prefix(&src_dir).unwrap_or_else(|e| panic!("{}", e)).to_owned()
    };

    // Make the copy.
    let walker = {
        WalkDir::new(&src_dir)
            .follow_links(true)
            .into_iter()
            .filter_entry(|entry| {
                // Save space on local builds by filtering out some of the worst offenders
                // on the exclude list from Cargo.toml.
                // (this does nothing for builds from crates.io)
                let blacklist = &[
                    Path::new("examples"),
                    Path::new("bench"),
                    Path::new("doc/src"),
                    Path::new("doc/util"),
                    Path::new("tools"),
                    Path::new("potentials"),
                ];

                !blacklist.contains(&suffix(&entry).as_ref())
            })
    };
    for entry in walker {
        let entry = entry?;
        let dest = copy.join(suffix(&entry));

        let ty = entry.file_type();
        if ty.is_file() {
            PathFile::new(entry.path()).unwrap_or_else(|e| panic!("{}", e)).copy(dest)?;
        } else if ty.is_dir() {
            PathDir::create(dest)?;
        }
    }

    // And now for the whole entire point of this function:
    //
    // The potentials/ directory needs to exist, but it does not exist
    // in the packaged crate file on crates.io.
    PathDir::create(copy.join("potentials"))?;

    Ok(copy)
}

cargo package now forbids the empty directory from being created, so the above workaround of copying the source directory is now necessary: