UP | HOME

Cleopatra Projects

Table of Contents

A cleopatra project is configured with a configuration file called cleopatra.toml.

1 The cleopatra.toml File

The cleopatra.toml file serves two purpose: it identifies the root of your project, and it contains configuration data that can be used by cleopatra.

The content of this file is encoded in Rust using the Config structure, with the following fields:

generation_processes PathBuf Path to the directory where the generation processes are defined  
env HashMap<String, OsString> Collection of environment variable to set before calling the generation processes serde(default)

We proceed with the methods implemented for the type Config.

2 find_project_then: Searching for a Project to Work With

find_project_then is a static method for Config which recursively looks for a configuration file (cleopatra.toml) up until the root of the filesystem. This function uses what we will call the with pattern: rather than returning a result for its caller to use, find_project_then takes a continuation as its argument, and call it once it has created an instance of Config.

pub fn find_project_then<A, K>(k : K) -> Result<A, Error>
where
    K : FnOnce(&Config) -> Result<A, Error>

Using this approach allows us to setting-up a particular execution context. More precisely, we use find_file to find a cleopatra.toml file. Then, we parse it as a TOML file, and chain helpers function which set-up the expected context, and restore the previous one once the continuation k returns.

let path = find_file("cleopatra.toml")?;
let project = Config::read_from_file(&path)?;
with_cwd(&project.root(), || with_env(&project.env, || k(&project)))

Therefore, find_project_then relies on several helpers, whose types and implementation are relatively straightforward.

read_from_file is defined as a method of the type Config. As a first step, it calls the toml crate to parse the content of the file passed as argument. Then, it postprocess it using prepare.

fn read_from_file(file : &Path) -> Result<Self, Error> {
    let content : String = std::fs::read_to_string(file)
        .map_err(Error::ConfigurationRead)?;

    let mut project = toml::from_str::<Config>(content.as_str())
        .map_err(|err| Error::ConfigurationParsing(file.into(), err))?;

    let parent = file.parent()
        .or_raise("cleopatra has found a configuration file, but could not guess the path of its directory.")?;
    project.prepare(parent)?;

    return Ok(project);
}

prepare is a method which takes a Config as deserialized by serde, and modifies it to be properly used by cleopatra afterwards. Indeed, cleopatra has some requirements that needs to be fulfill:

  • A ROOT entry in the env hash map which points to the root of the cleopatra project
  • A CLEOPATRA_DIRECTORY in the env hash map wihch is set to ${ROOT}/.cleopatra
  • A CLEOPATRA_GENERATION_PROCESSES in the env hash map wihch is set to project.generation_processes
  • A PATH entry in the env hash map which adds ${ROOT}/.cleopatra/bin to the current PATH
fn prepare(&mut self, root : &Path) -> Result<(), Error> {
    self.env.insert(
        String::from("ROOT"),
        OsString::from(root)
    );

    self.env.insert(
        String::from("CLEOPATRA_DIRECTORY"),
        OsString::from(root.join(".cleopatra"))
    );

    self.env.insert(
        String::from("CLEOPATRA_GENERATION_PROCESSES"),
        OsString::from(self.generation_processes.clone())
    );

    std::env::var_os("PATH")
        .as_ref()
        .map(|val| {
            let mut paths = std::env::split_paths(val).collect::<Vec<_>>();
            paths.push(root.join(".cleopatra/bin/"));
            self.env.insert(
                String::from("PATH"),
                std::env::join_paths(paths)
                    .unwrap_or(root.join(".cleopatra/bin/").into())
            )
        });

    Ok(())
}

find_file is a regular function which recursively search for a file, from the current directory up to the root of the filesystem.

fn find_file(filename : &str) -> Result<PathBuf, Error> {
    let mut cwd : PathBuf = std::env::current_dir()
        .or_raise("Cannot get current directory")?;

    loop {
        let candidate = cwd.join(filename);

        if candidate.exists() {
            return Ok(candidate);
        }

        if !cwd.pop() {
            return Err(Error::ConfigurationNotFound);
        }
    }
}

The other helpers aim to set-up the execution context of the find_project_then continuation, and are defined as regular functions private to the configuration module.

fn with_cwd<K, A>(target : &Path, k : K) -> Result<A, Error>
where
    K : FnOnce() -> Result<A, Error> {
    let origin : PathBuf = std::env::current_dir()
        .or_raise("Cannot get current directory")?;

    std::env::set_current_dir(target)
        .or_raise(&format!("Could not move to the directory {:?}", target))?;

    let res = k();

    std::env::set_current_dir(origin)
        .or_raise(&format!("Could not return from the directory {:?}", target))?;

    return res;
}
fn with_env<K, A>(env : &HashMap<String, OsString>, k : K) -> Result<A, Error>
where
    K : FnOnce() -> Result<A, Error> {
    let context : HashMap<&String, Option<OsString>> = env
        .iter()
        .map(|(var, val)| {
            let old = std::env::var_os(var);
            std::env::set_var(var, val);
            (var, old)
        })
        .collect();

    let res = k();

    for (var, old) in context {
        match old {
            Some(val) => std::env::set_var(var, val),
            None => std::env::remove_var(var),
        }
    }

    return res;
}

3 root: Retreive the root directory of a project

We provide a public method root, such that conf.root() returns the path of the root directory of the considered project.

pub fn root(&self) -> PathBuf

The implementation is currently potentially unsafe if the Config object has not be constructed correctly. Indeed, we assume that the env hash map has a value for the ROOT key, which is the case with find_project_then.

self.env["ROOT"].clone().into()

4 prepare_workspace

We provide prepare_workspace, a method for creating the .cleopatra directory used by cleopatra to store certain files it used at runtime.

We include the content of these files within a dedicated, static (thanks to the macro of the lazy_static crate) hash map called FILES. The current way we define FILES is cumbersome, and could easily be improved.

lazy_static! {
    static ref FILES : HashMap<PathBuf, (&'static[u8], bool)> = {
        let mut res = HashMap::new();

        res.insert(
            PathBuf::from(".cleopatra/boot.mk"),
            (&include_bytes!("../boot.mk")[..], false),
        );

        res.insert(
            PathBuf::from(".cleopatra/emacs.d/cleopatra.el"),
            (&include_bytes!("../emacs.d/cleopatra.el")[..], false),
        );

        res.insert(
            PathBuf::from(".cleopatra/emacs.d/cleopatra-gen-proc.el"),
            (&include_bytes!("../emacs.d/cleopatra-gen-proc.el")[..], false),
        );

        res.insert(
            PathBuf::from(".cleopatra/bin/cleopatra-emacs"),
            (&include_bytes!("../bin/cleopatra-emacs")[..], true),
        );

        res.insert(
            PathBuf::from(".cleopatra/bin/cleopatra-update-gitignore"),
            (&include_bytes!("../bin/cleopatra-update-gitignore")[..], true),
        );

        res.insert(
            PathBuf::from(".cleopatra/bin/cleopatra-gen-deps"),
            (&include_bytes!("../bin/cleopatra-gen-deps")[..], true),
        );

        res.insert(
            PathBuf::from(".cleopatra/bin/cleopatra-run-elisp"),
            (&include_bytes!("../bin/cleopatra-run-elisp")[..], true),
        );

        res
    };
}

The business logic of prepare_workspace is implemented in a private method called prepare_workspace_io, whose only difference with prepare_workspace is the type of its errors. prepare_workspace_io returns a FS error, that we turn in a cleopatra error using the InitWorkingDirectory variant.

pub fn prepare_workspace(&self) -> Result<(), Error> {
    self.prepare_workspace_io().map_err(Error::InitWorkingDirectory)
}

The implementation of prepare_workspace_io consists in iterating the content of FILES. Note that we use the set_permissions which is Unix specific. Therefore, cleopatra does not work on Windows for now.

fn prepare_workspace_io(&self) -> Result<(), std::io::Error> {
    for (path, content) in FILES.iter() {
        if !path.exists() {
            create_dir_all(path.parent().unwrap())?;
            write(path, content.0)?;
            if content.1 {
                let mut perms = std::fs::metadata(path)?.permissions();
                perms.set_mode(0o755);
                std::fs::set_permissions(path, perms)?;
            }
        }
    }

    Ok(())
}

Author: Thomas Letan

Created: 2020-12-20 Sun 12:56

Validate