├── test ├── lua │ ├── test3.lua │ └── tale │ │ └── test2.lua ├── test.bash └── test.lua ├── .clang-format ├── docs ├── source │ ├── internal │ │ ├── rb_ctx.md │ │ ├── rb_idlist.md │ │ ├── rb_helpers.md │ │ ├── rb_strlist.md │ │ └── index.md │ ├── c_api │ │ ├── librootbeer.md │ │ ├── index.md │ │ └── rb_plugin.md │ ├── index.rst │ └── conf.py └── requirements.txt ├── .zed └── settings.json ├── src ├── rootbeer_cli │ ├── src │ │ ├── main.c │ │ ├── util │ │ │ ├── log.c │ │ │ └── fs.c │ │ ├── cli_tool │ │ │ ├── command.c │ │ │ └── parse.c │ │ ├── rev_store │ │ │ ├── current.c │ │ │ ├── view.c │ │ │ ├── manage.c │ │ │ ├── read.c │ │ │ └── create.c │ │ ├── lua_config │ │ │ ├── rb_ctx_state.c │ │ │ └── lua_init.c │ │ └── cli_cmds │ │ │ ├── apply.c │ │ │ └── store.c │ ├── gen_cmd_array │ │ ├── meson.build │ │ └── gen_cmd_array.py │ ├── include │ │ ├── rootbeer.h │ │ ├── cli_module.h │ │ ├── rb_ctx_state.h │ │ ├── lua_init.h │ │ └── store_module.h │ └── meson.build ├── plugins │ ├── homebrew │ │ ├── meson.build │ │ └── src │ │ │ └── core.c │ ├── rpm_pkg │ │ ├── src │ │ │ ├── rpm_pkg.h │ │ │ ├── plugin.c │ │ │ ├── query.c │ │ │ └── solv │ │ │ │ └── repo_loader.c │ │ └── meson.build │ ├── rootbeer_core │ │ ├── src │ │ │ ├── rootbeer_core.h │ │ │ ├── core.c │ │ │ ├── interpolate_table.c │ │ │ ├── register_module.c │ │ │ ├── to_json.c │ │ │ ├── link_file.c │ │ │ └── write_file.c │ │ └── meson.build │ ├── meson.build │ └── gen_plugin_registry │ │ ├── gen_plugin_registry.py │ │ └── meson.build ├── librootbeer │ ├── meson.build │ └── src │ │ ├── helpers │ │ ├── canon.c │ │ ├── strlist.c │ │ └── idlist.c │ │ └── api │ │ ├── track_gen_file.c │ │ ├── track_ref_file.c │ │ ├── execute_command.c │ │ └── intermediate.c └── internal_include │ ├── rb_helpers.h │ ├── rb_strlist.h │ ├── rb_idlist.h │ └── rb_ctx.h ├── .gitignore ├── lua └── rootbeer │ └── shells │ ├── init.lua │ └── zsh.lua ├── meson.build ├── .github └── workflows │ ├── clang-format-check.yml │ └── build.yml ├── subprojects ├── cjson.wrap └── luajit.wrap ├── LICENSE ├── mise.toml ├── flake.lock ├── guix.scm ├── include ├── rb_plugin.h └── rb_rootbeer.h ├── README.md └── flake.nix /test/lua/test3.lua: -------------------------------------------------------------------------------- 1 | print("Invoked via a require to test3.lua") 2 | -------------------------------------------------------------------------------- /test/lua/tale/test2.lua: -------------------------------------------------------------------------------- 1 | print("Invoked via a require to tale/test2.lua") 2 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | UseTab: Always 3 | IndentWidth: 4 4 | TabWidth: 4 5 | -------------------------------------------------------------------------------- /docs/source/internal/rb_ctx.md: -------------------------------------------------------------------------------- 1 | # `rb_ctx.h` 2 | ```{doxygenfile} src/internal_include/rb_ctx.h 3 | -------------------------------------------------------------------------------- /docs/source/internal/rb_idlist.md: -------------------------------------------------------------------------------- 1 | # `rb_idlist.h` 2 | ```{doxygenfile} src/internal_include/rb_idlist.h 3 | -------------------------------------------------------------------------------- /docs/source/internal/rb_helpers.md: -------------------------------------------------------------------------------- 1 | # `rb_helpers.h` 2 | ```{doxygenfile} src/internal_include/rb_helpers.h 3 | -------------------------------------------------------------------------------- /docs/source/internal/rb_strlist.md: -------------------------------------------------------------------------------- 1 | # `rb_strlist.h` 2 | ```{doxygenfile} src/internal_include/rb_strlist.h 3 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | breathe~=4.36 2 | furo~=2024.8 3 | myst-parser~=4.0 4 | sphinx~=8.2 5 | sphinx-lua-ls~=2.0 6 | -------------------------------------------------------------------------------- /docs/source/c_api/librootbeer.md: -------------------------------------------------------------------------------- 1 | # Rootbeer API 2 | ```{doxygenfile} include/rb_rootbeer.h 3 | :project: Rootbeer 4 | -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "languages": { 3 | "YAML": { 4 | "tab_size": 2, 5 | "hard_tabs": false 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/source/c_api/index.md: -------------------------------------------------------------------------------- 1 | # C API Reference 2 | 3 | ```{toctree} 4 | :caption: Contents 5 | :maxdepth: 1 6 | 7 | rb_plugin 8 | librootbeer 9 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/main.c: -------------------------------------------------------------------------------- 1 | #include "cli_module.h" 2 | 3 | int main(const int argc, const char *argv[]) { 4 | return rb_cli_main(argc, argv); 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .cache/ 3 | .ropeproject/ 4 | .venv/ 5 | docs/.doxygen/ 6 | docs/_build 7 | subprojects/* 8 | !subprojects/*.wrap 9 | result 10 | -------------------------------------------------------------------------------- /test/test.bash: -------------------------------------------------------------------------------- 1 | # This file doesn't do anything 2 | # But we test that it's being referenced in our lua config! 3 | function test() { 4 | echo "Hello" 5 | } 6 | -------------------------------------------------------------------------------- /lua/rootbeer/shells/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local zsh = require("rootbeer.shells.zsh") 3 | 4 | function M.create_zsh_config(config) 5 | return zsh.create_config(config) 6 | end 7 | 8 | return M 9 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/util/log.c: -------------------------------------------------------------------------------- 1 | #include "rootbeer.h" 2 | 3 | void rb_fatal(const char *format, ...) { 4 | fprintf(stderr, "Fatal Error: "); 5 | 6 | va_list args; 7 | va_start(args, format); 8 | vfprintf(stderr, format, args); 9 | va_end(args); 10 | 11 | fprintf(stderr, "\n"); 12 | exit(EXIT_FAILURE); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/plugins/homebrew/meson.build: -------------------------------------------------------------------------------- 1 | homebrew_lib = static_library( 2 | 'homebrew_plugin', 3 | sources: files( 4 | 'src/core.c', 5 | ), 6 | include_directories: [include_directories('src'), root_include], 7 | dependencies: [dependency('luajit')], 8 | ) 9 | 10 | register_plugin_lib = homebrew_lib 11 | register_plugin_name = 'brew' 12 | -------------------------------------------------------------------------------- /docs/source/internal/index.md: -------------------------------------------------------------------------------- 1 | # Internal API 2 | :::{warning} 3 | This is a public documentation of the internal API for Rootbeer. 4 | It's not intended to be used publicly and exists to keep track of 5 | documented features. 6 | ::: 7 | 8 | ```{toctree} 9 | :maxdepth: 1 10 | :caption: Contents 11 | rb_ctx 12 | rb_idlist 13 | rb_strlist 14 | rb_helpers 15 | -------------------------------------------------------------------------------- /src/plugins/rpm_pkg/src/rpm_pkg.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define MAX_REPOS 128 5 | #define DNF5_CACHE "/var/cache/libdnf5" 6 | 7 | typedef struct { 8 | char *name; 9 | char *evr; 10 | char *arch; 11 | char *repo; 12 | char *rpm_url; 13 | } rpm_pkg_t; 14 | 15 | void query_dnf_packages(char **packages, size_t count); 16 | Pool *load_all_solv_repos(const char *solv_root); 17 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/cli_tool/command.c: -------------------------------------------------------------------------------- 1 | #include "cli_module.h" 2 | 3 | void rb_cli_print_help() { 4 | puts("rootbeer: Deterministically manage your system using Lua!"); 5 | puts("Usage: rootbeer [options]"); 6 | puts("Commands:"); 7 | 8 | for (int i = 0; rb_cli_cmds[i] != NULL; i++) { 9 | rb_cli_cmd *cmd = rb_cli_cmds[i]; 10 | printf(" %s: %s\n", cmd->name, cmd->description); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/plugins/rpm_pkg/meson.build: -------------------------------------------------------------------------------- 1 | libsolv_dep = dependency('libsolv', required: true, native: true) 2 | 3 | rpm_pkg_lib = static_library( 4 | 'rpm_pkg_plugin', 5 | sources: files('src/plugin.c', 'src/query.c', 'src/solv/repo_loader.c'), 6 | include_directories: [include_directories('src'), root_include], 7 | dependencies: [dependency('luajit'), libsolv_dep], 8 | ) 9 | 10 | register_plugin_lib = rpm_pkg_lib 11 | register_plugin_name = 'rpm_pkg' 12 | -------------------------------------------------------------------------------- /src/plugins/rootbeer_core/src/rootbeer_core.h: -------------------------------------------------------------------------------- 1 | #ifndef ROOTBEER_CORE_H 2 | #define ROOTBEER_CORE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | int rb_core_ref_file(lua_State *L); 9 | int rb_core_link_file(lua_State *L); 10 | int rb_core_to_json(lua_State *L); 11 | int rb_core_write_file(lua_State *L); 12 | int rb_core_interpolate_table(lua_State *L); 13 | int rb_core_register_module(lua_State *L); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. _rootbeer-docs: 2 | 3 | Rootbeer 4 | ======== 5 | 6 | Deterministically manage your system using Lua! 7 | 8 | Welcome to the documentation for **Rootbeer**! As of now, this site is not 9 | complete and does not include installation/usage instructions. Its main purpose 10 | is to provide API references for the C runtime and the Lua modules. 11 | 12 | 📚 Table of Contents 13 | -------------------- 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | :caption: Contents 18 | 19 | c_api/index 20 | internal/index 21 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'rootbeer', 3 | 'c', 4 | version: '0.0.1', 5 | license: 'MIT', 6 | default_options: [ 7 | 'c_std=gnu99', 8 | 'buildtype=debugoptimized', 9 | 'warning_level=2', 10 | ], 11 | ) 12 | root_include = include_directories('include') 13 | internal_include = include_directories('src/internal_include') 14 | 15 | if get_option('buildtype').startswith('debug') 16 | add_project_arguments('-DDEBUG', language: 'c') 17 | endif 18 | 19 | subdir('src/librootbeer') 20 | subdir('src/plugins') 21 | subdir('src/rootbeer_cli') 22 | -------------------------------------------------------------------------------- /src/plugins/meson.build: -------------------------------------------------------------------------------- 1 | plugin_dirs = ['rootbeer_core', 'rpm_pkg'] 2 | 3 | plugin_sources = [] 4 | plugin_names = [] 5 | plugin_libs = [] 6 | 7 | foreach d : plugin_dirs 8 | subdir(d) 9 | if is_variable('register_plugin_sources') 10 | plugin_sources += register_plugin_sources 11 | endif 12 | 13 | if is_variable('register_plugin_name') 14 | plugin_names += register_plugin_name 15 | endif 16 | 17 | if is_variable('register_plugin_lib') 18 | plugin_libs += register_plugin_lib 19 | endif 20 | endforeach 21 | 22 | subdir('gen_plugin_registry') 23 | -------------------------------------------------------------------------------- /src/rootbeer_cli/gen_cmd_array/meson.build: -------------------------------------------------------------------------------- 1 | if not is_variable('cli_sources') 2 | error( 3 | '%s is expected to be defined with %', 4 | '`cli_sources`', 5 | 'a `files()` call of all the CLI command source files', 6 | ) 7 | endif 8 | 9 | py_script = find_program('gen_cmd_array.py', required: true) 10 | cmd_array_gen = custom_target( 11 | 'gen_cmd_array', 12 | input: cli_sources, 13 | output: 'cmd_array.c', 14 | command: [ 15 | py_script, 16 | '@OUTPUT@', 17 | '@INPUT@', 18 | ], 19 | ) 20 | 21 | cli_taped_sources = cli_sources + [cmd_array_gen] 22 | -------------------------------------------------------------------------------- /src/plugins/gen_plugin_registry/gen_plugin_registry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | outfile = sys.argv[1] 5 | sources = sys.argv[2:] 6 | 7 | with open(outfile, 'w') as f: 8 | f.write("// Auto-generated by Meson\n") 9 | f.write('#include "rb_plugin.h"\n\n') 10 | 11 | for name in sources: 12 | f.write(f"extern const rb_plugin_t rb_plugin_{name};\n") 13 | 14 | f.write("\nconst rb_plugin_t *rb_plugins[] = {\n") 15 | for name in sources: 16 | f.write(f" &rb_plugin_{name},\n") 17 | f.write(" NULL\n};\n") 18 | -------------------------------------------------------------------------------- /src/librootbeer/meson.build: -------------------------------------------------------------------------------- 1 | librootbeer_sources = files( 2 | 'src/api/intermediate.c', 3 | 'src/api/track_gen_file.c', 4 | 'src/api/track_ref_file.c', 5 | 'src/helpers/canon.c', 6 | 'src/helpers/idlist.c', 7 | 'src/helpers/strlist.c', 8 | ) 9 | 10 | librootbeer = static_library( 11 | 'rootbeer', 12 | librootbeer_sources, 13 | dependencies: [dependency('luajit')], 14 | include_directories: [root_include, internal_include], 15 | ) 16 | 17 | librootbeer_dep = declare_dependency( 18 | link_with: librootbeer, 19 | include_directories: [root_include, internal_include], 20 | ) 21 | 22 | # rootbeer_sources += [librootbeer] 23 | -------------------------------------------------------------------------------- /.github/workflows/clang-format-check.yml: -------------------------------------------------------------------------------- 1 | name: clang-format Check 2 | on: [push, pull_request] 3 | jobs: 4 | formatting-check: 5 | name: Formatting Check 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | path: 10 | - "src" 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Run clang-format style check for C/C++/Protobuf programs. 14 | uses: jidicula/clang-format-action@v4.14.0 15 | continue-on-error: true 16 | with: 17 | clang-format-version: "13" 18 | check-path: ${{ matrix.path }} 19 | fallback-style: "LLVM" # optional 20 | -------------------------------------------------------------------------------- /src/rootbeer_cli/include/rootbeer.h: -------------------------------------------------------------------------------- 1 | #ifndef ROOTBEER_H 2 | #define ROOTBEER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | // Utility functions (which are assert friendly) 17 | int rb_create_dir(char *path); 18 | int rb_copy_file(const char *src, const char *dst); 19 | char **rb_recurse_files(const char *path, int *count); 20 | 21 | void rb_fatal(const char *format, ...); 22 | 23 | #endif // ROOTBEER_H 24 | 25 | -------------------------------------------------------------------------------- /src/plugins/rootbeer_core/meson.build: -------------------------------------------------------------------------------- 1 | rootbeer_lib = static_library( 2 | 'rootbeer_plugin', 3 | sources: files( 4 | 'src/core.c', 5 | 'src/interpolate_table.c', 6 | 'src/link_file.c', 7 | 'src/register_module.c', 8 | 'src/to_json.c', 9 | 'src/write_file.c', 10 | ), 11 | c_args: ['-DRB_PLUGIN_OVERRIDES_INTERNAL'], 12 | # This is the core rootbeer plugin, it has access to the real rb_ctx_t 13 | include_directories: [include_directories('src'), root_include, internal_include], 14 | dependencies: [dependency('libcjson'), dependency('luajit')], 15 | ) 16 | 17 | register_plugin_lib = rootbeer_lib 18 | register_plugin_name = '__rootbeer__' 19 | -------------------------------------------------------------------------------- /src/rootbeer_cli/gen_cmd_array/gen_cmd_array.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | 5 | outfile = sys.argv[1] 6 | sources = sys.argv[2:] 7 | 8 | with open(outfile, 'w') as f: 9 | f.write("// Auto-generated by Meson\n") 10 | f.write('#include "cli_module.h"\n\n') 11 | 12 | names = [] 13 | for path in sources: 14 | name = os.path.splitext(os.path.basename(path))[0] 15 | f.write(f"extern rb_cli_cmd {name};\n") 16 | names.append(name) 17 | 18 | f.write("\nrb_cli_cmd *rb_cli_cmds[] = {\n") 19 | for name in names: 20 | f.write(f" &{name},\n") 21 | f.write(" NULL\n};\n") 22 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/cli_tool/parse.c: -------------------------------------------------------------------------------- 1 | #include "cli_module.h" 2 | 3 | int rb_cli_main(const int argc, const char *argv[]) { 4 | if (argc < 2) { 5 | rb_cli_print_help(); 6 | return 1; 7 | } 8 | 9 | for (int i = 0; rb_cli_cmds[i] != NULL; i++) { 10 | rb_cli_cmd *cmd = rb_cli_cmds[i]; 11 | if (strcmp(argv[1], cmd->name) == 0) { 12 | // At this moment each of our commands have their own subcommands 13 | // so if they have 0 arguments we call cmd.print_usage() instead 14 | if (argc == 2) { 15 | cmd->print_usage(); 16 | return 0; 17 | } 18 | 19 | return cmd->func(argc, argv); 20 | } 21 | } 22 | 23 | rb_cli_print_help(); 24 | return 0; 25 | } 26 | -------------------------------------------------------------------------------- /subprojects/cjson.wrap: -------------------------------------------------------------------------------- 1 | [wrap-file] 2 | directory = cJSON-1.7.18 3 | source_url = https://github.com/DaveGamble/cJSON/archive/refs/tags/v1.7.18.tar.gz 4 | source_filename = cJSON-1.7.18.tar.gz 5 | source_hash = 3aa806844a03442c00769b83e99970be70fbef03735ff898f4811dd03b9f5ee5 6 | patch_filename = cjson_1.7.18-2_patch.zip 7 | patch_url = https://wrapdb.mesonbuild.com/v2/cjson_1.7.18-2/get_patch 8 | patch_hash = 2751e96f2cea6403f4cd8e9c2ad7834bb614345e1d8db6488c02ded3fefe7245 9 | source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/cjson_1.7.18-2/cJSON-1.7.18.tar.gz 10 | wrapdb_version = 1.7.18-2 11 | 12 | [provide] 13 | libcjson = libcjson_dep 14 | libcjson_utils = libcjson_utils_dep 15 | -------------------------------------------------------------------------------- /lua/rootbeer/shells/zsh.lua: -------------------------------------------------------------------------------- 1 | local rb = require("rootbeer") 2 | local M = {} 3 | 4 | function M.create_config(config) 5 | local zsh_conf = rb.interpolate_table(config, function(cfg) 6 | local zshrc = "" 7 | 8 | -- Add environment variables 9 | if cfg.env then 10 | for key, value in pairs(cfg.env) do 11 | zshrc = zshrc .. "export " .. key .. "=\"" .. value .. "\"\n" 12 | end 13 | end 14 | 15 | -- Add aliases 16 | if cfg.aliases then 17 | for alias, command in pairs(cfg.aliases) do 18 | zshrc = zshrc .. "alias " .. alias .. "=\"" .. command .. "\"\n" 19 | end 20 | end 21 | 22 | return zshrc 23 | end) 24 | 25 | return rb.write_file("./test/.zshrc", zsh_conf) 26 | end 27 | 28 | return M 29 | -------------------------------------------------------------------------------- /src/librootbeer/src/helpers/canon.c: -------------------------------------------------------------------------------- 1 | #include "rb_helpers.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | char *rb_canon_relative(rb_ctx_t *ctx, const char *abs_path) { 8 | if (ctx == NULL || abs_path == NULL) { 9 | return NULL; 10 | } 11 | 12 | // Developer error misconfiguration 13 | assert(ctx->script_dir != NULL); 14 | char res_path[PATH_MAX]; 15 | if (!realpath(abs_path, res_path)) { 16 | return NULL; 17 | } 18 | 19 | size_t dir_len = strlen(ctx->script_dir); 20 | if (strncmp(res_path, ctx->script_dir, dir_len) != 0 || res_path[dir_len] != '/') { 21 | // Not relative, we can just return the absolute path 22 | return strdup(res_path); 23 | } 24 | 25 | return strdup(res_path + dir_len + 1); 26 | } 27 | -------------------------------------------------------------------------------- /src/librootbeer/src/api/track_gen_file.c: -------------------------------------------------------------------------------- 1 | #include "rb_ctx.h" 2 | #include "rb_rootbeer.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int rb_track_gen_file(rb_ctx_t *ctx, const char *path) { 9 | if (ctx->plugin_transforms_count >= RB_CTX_TRANSFORMS_MAX) { 10 | return RB_ULIMIT_TRANSFORMS; 11 | } 12 | 13 | if (access(path, R_OK | W_OK) != 0) { 14 | // Check if the errno is EACCES, since we can ignore ENOENT 15 | if (errno == EACCES) { 16 | return RB_EACCES; 17 | } 18 | } 19 | 20 | ctx->plugin_transforms[ctx->plugin_transforms_count] = malloc(strlen(path) + 1); 21 | strncpy(ctx->plugin_transforms[ctx->plugin_transforms_count], path, strlen(path) + 1); 22 | ctx->plugin_transforms_count++; 23 | return RB_OK; 24 | } 25 | -------------------------------------------------------------------------------- /subprojects/luajit.wrap: -------------------------------------------------------------------------------- 1 | [wrap-file] 2 | directory = LuaJIT-04dca7911ea255f37be799c18d74c305b921c1a6 3 | source_url = https://github.com/LuaJIT/LuaJIT/archive/04dca7911ea255f37be799c18d74c305b921c1a6.tar.gz 4 | source_filename = luajit-2.1.1720049189.tar.gz 5 | source_hash = 346b028d9ba85e04b7e23a43cc51ec076574d2efc0d271d4355141b0145cd6e0 6 | patch_filename = luajit_2.1.1720049189-3_patch.zip 7 | patch_url = https://wrapdb.mesonbuild.com/v2/luajit_2.1.1720049189-3/get_patch 8 | patch_hash = f08a308599ff48ad73a8020ea592f1cd0f0b19bd9e9246a10503888d51889fe3 9 | source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/luajit_2.1.1720049189-3/luajit-2.1.1720049189.tar.gz 10 | wrapdb_version = 2.1.1720049189-3 11 | 12 | [provide] 13 | dependency_names = luajit 14 | program_names = luajit 15 | -------------------------------------------------------------------------------- /src/plugins/gen_plugin_registry/meson.build: -------------------------------------------------------------------------------- 1 | if not is_variable('plugin_sources') 2 | error( 3 | '%s is expected to be defined with %', 4 | '`plugin_sources`', 5 | 'a `files()` call of all the plugin source files', 6 | ) 7 | endif 8 | 9 | if not is_variable('plugin_names') 10 | error( 11 | '%s is expected to be defined with %', 12 | '`plugin_names`', 13 | 'a `string_array()` call of all the plugin names', 14 | ) 15 | endif 16 | 17 | py_script = find_program('gen_plugin_registry.py', required: true) 18 | message('Plugins to register: ' + ', '.join(plugin_names)) 19 | 20 | plugin_registry_gen = custom_target( 21 | 'gen_plugin_registry', 22 | output: 'plugin_registry.c', 23 | command: [py_script, '@OUTPUT@'] + plugin_names, 24 | ) 25 | 26 | plugin_taped_sources = plugin_sources + [plugin_registry_gen] 27 | -------------------------------------------------------------------------------- /src/rootbeer_cli/meson.build: -------------------------------------------------------------------------------- 1 | cli_sources = files( 2 | 'src/cli_cmds/apply.c', 3 | 'src/cli_cmds/store.c', 4 | ) 5 | 6 | subdir('gen_cmd_array') 7 | src_files = cli_taped_sources + plugin_taped_sources + files( 8 | 'src/cli_tool/command.c', 9 | 'src/cli_tool/parse.c', 10 | 11 | 'src/lua_config/lua_init.c', 12 | 'src/lua_config/rb_ctx_state.c', 13 | 14 | 'src/main.c', 15 | 16 | 'src/rev_store/create.c', 17 | 'src/rev_store/current.c', 18 | 'src/rev_store/manage.c', 19 | 'src/rev_store/read.c', 20 | 'src/rev_store/view.c', 21 | 22 | 'src/util/fs.c', 23 | 'src/util/log.c', 24 | ) 25 | 26 | executable( 27 | 'rb', 28 | src_files, 29 | dependencies: [dependency('luajit'), librootbeer_dep], 30 | link_with: plugin_libs, 31 | include_directories: [ 32 | include_directories('include'), 33 | root_include, 34 | internal_include, 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /src/internal_include/rb_helpers.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rb_helpers.h 3 | * @brief Contains various helper functions for the `librootbeer` API. 4 | * 5 | * General purpose functions that can easily be extracted for reuse. 6 | * Good examples are @ref rb_canon_relative which canonicalizes an absolute 7 | * path to a relative path, etc. 8 | */ 9 | #ifndef RB_HELPERS_H 10 | #define RB_HELPERS_H 11 | 12 | #include 13 | 14 | /** 15 | * Canonicalizes an absolute path to a relative path based on the context. 16 | * The path is relative to the entrypoint lua script directory. 17 | * 18 | * @param ctx Pointer to the rootbeer context. 19 | * @param abs_path The absolute path to be canonicalized. 20 | * @return A newly allocated string containing the relative path. 21 | */ 22 | char *rb_canon_relative(rb_ctx_t *ctx, const char *abs_path); 23 | 24 | #endif // RB_HELPERS_H 25 | -------------------------------------------------------------------------------- /src/plugins/rootbeer_core/src/core.c: -------------------------------------------------------------------------------- 1 | #include "rootbeer_core.h" 2 | #include "rb_ctx.h" 3 | 4 | int hello_world(lua_State *L) { 5 | lua_pushstring(L, "Hello, World!"); 6 | return 1; 7 | } 8 | 9 | int rb_core_ref_file(lua_State *L) { 10 | const char *filename = luaL_checkstring(L, 1); 11 | rb_ctx_t *ctx = rb_ctx_from_lua(L); 12 | 13 | int status = rb_track_ref_file(ctx, (char *)filename); 14 | if (status != 0) { 15 | return luaL_error(L, "Failed to track file: %s", filename); 16 | } 17 | 18 | return 0; 19 | } 20 | 21 | const luaL_Reg functions[] = { 22 | {"hello_world", hello_world}, 23 | {"ref_file", rb_core_ref_file}, 24 | {"link_file", rb_core_link_file}, 25 | {"to_json", rb_core_to_json}, 26 | {"write_file", rb_core_write_file}, 27 | {"interpolate_table", rb_core_interpolate_table}, 28 | {"register_module", rb_core_register_module}, 29 | {NULL, NULL} 30 | }; 31 | 32 | RB_PLUGIN(__rootbeer__, "a bobr rootbeer", "1.0.0", functions) 33 | -------------------------------------------------------------------------------- /src/librootbeer/src/api/track_ref_file.c: -------------------------------------------------------------------------------- 1 | #include "rb_ctx.h" 2 | #include "rb_rootbeer.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | int rb_track_ref_file(rb_ctx_t *ctx, const char *path) { 10 | if (ctx->ext_files_count >= RB_CTX_EXTFILES_MAX) { 11 | return RB_ULIMIT_EXTFILES; 12 | } 13 | 14 | // See if we can even resolve the file or not 15 | char filename[strlen(ctx->script_dir) + strlen(path) + 2]; 16 | snprintf(filename, sizeof(filename), "%s/%s", ctx->script_dir, path); 17 | 18 | char resolved_path[PATH_MAX]; 19 | if (realpath(filename, resolved_path) == NULL) { 20 | return RB_ENOENT; 21 | } 22 | 23 | if (access(resolved_path, F_OK | R_OK) != 0) { 24 | return RB_EACCES; 25 | } 26 | 27 | ctx->ext_files[ctx->ext_files_count] = malloc(strlen(filename) + 1); 28 | strncpy(ctx->ext_files[ctx->ext_files_count], filename, strlen(filename) + 1); 29 | ctx->ext_files_count++; 30 | return RB_OK; 31 | } 32 | -------------------------------------------------------------------------------- /src/plugins/rootbeer_core/src/interpolate_table.c: -------------------------------------------------------------------------------- 1 | #include "rootbeer_core.h" 2 | 3 | // Takes in a table of values and a lua function to return the interpolated value 4 | int rb_core_interpolate_table(lua_State *L) { 5 | luaL_checktype(L, 1, LUA_TTABLE); 6 | luaL_checktype(L, 2, LUA_TFUNCTION); 7 | 8 | lua_pushvalue(L, 2); 9 | lua_pushvalue(L, 1); // Pass the table to the function 10 | 11 | if (lua_pcall(L, 1, 1, 0) != LUA_OK) { 12 | const char *error = lua_tostring(L, -1); 13 | lua_pop(L, 1); // Remove the error message from the stack 14 | luaL_error(L, "Error in interpolation function: %s", error); 15 | return 0; 16 | } 17 | 18 | if (!lua_isstring(L, -1)) { 19 | luaL_error(L, "Interpolation function must return a string"); 20 | return 0; 21 | } 22 | 23 | // Return the string result 24 | const char *result = lua_tostring(L, -1); 25 | lua_pop(L, 1); // Remove the result from the stack 26 | lua_pushstring(L, result); 27 | return 1; // Return the interpolated string 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aarnav Tale 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | _.path = ["./build/src/rootbeer_cli"] 3 | _.python.venv = { path = ".venv", create = true } 4 | 5 | [tools] 6 | meson = "latest" 7 | ninja = "1.12" 8 | python = "latest" 9 | lua-language-server = "latest" 10 | 11 | [tasks.setup] 12 | description = "Setup the project" 13 | run = "test -d build || meson setup build" 14 | alias = "s" 15 | 16 | [tasks.build] 17 | description = "Build the project" 18 | run = "meson compile -C build" 19 | depends = ["setup"] 20 | alias = "b" 21 | 22 | [tasks.clean] 23 | description = "Clean the project" 24 | run = "meson setup build --wipe" 25 | alias = "c" 26 | 27 | [tasks.install-venv-deps] 28 | description = "Install Python dependencies in the virtual environment" 29 | run = "pip install -r docs/requirements.txt" 30 | 31 | [tasks.docs-ci] 32 | description = "Generate documentation (for CI)" 33 | dir = "docs" 34 | run = "sphinx-build -b html source _build/html" 35 | depends = ["install-venv-deps"] 36 | 37 | [tasks.docs] 38 | description = "Generate documentation" 39 | dir = "docs" 40 | run = "doxygen && sphinx-build -b html source _build/html" 41 | depends = ["install-venv-deps"] 42 | alias = "d" 43 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/rev_store/current.c: -------------------------------------------------------------------------------- 1 | #include "store_module.h" 2 | 3 | // Returns the current revision. 4 | rb_revision_t *rb_store_get_current_revision() { 5 | char *current_path = malloc(strlen(STORE_ROOT) + strlen("/_current") + 1); 6 | strcpy(current_path, STORE_ROOT); 7 | strcat(current_path, "/_current"); 8 | 9 | char buffer[256]; 10 | FILE *file = fopen(current_path, "r"); 11 | if (file == NULL) { 12 | return NULL; 13 | } 14 | 15 | fgets(buffer, 256, file); 16 | fclose(file); 17 | 18 | int id; 19 | int res = sscanf(buffer, "%d", &id); 20 | if (res != 1) { 21 | return NULL; 22 | } 23 | 24 | return rb_store_get_revision_by_id(id); 25 | } 26 | 27 | // Sets the current revision. 28 | int rb_store_set_current_revision(const int id) { 29 | FILE *file = fopen(STORE_ROOT "/_current", "w"); 30 | if (file == NULL) { 31 | return 1; 32 | } 33 | 34 | fprintf(file, "%d", id); 35 | fclose(file); 36 | return 0; 37 | } 38 | 39 | // Returns the ID for the next revision. 40 | int rb_store_next_id() { 41 | rb_revision_t *rev = rb_store_get_current_revision(); 42 | if (rev == NULL) { 43 | return 0; 44 | } 45 | 46 | return rev->id + 1; 47 | } 48 | -------------------------------------------------------------------------------- /src/rootbeer_cli/include/cli_module.h: -------------------------------------------------------------------------------- 1 | #ifndef CLI_MODULE_H 2 | #define CLI_MODULE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // Main CLI entrypoint for the application, essentially what main will 11 | // always pass itself through in order to actually understand what it needs 12 | // to do for the lifecycle of its execution. 13 | 14 | int rb_cli_main(const int argc, const char *argv[]); 15 | void rb_cli_print_help(); 16 | 17 | // Stores all commands for easy lookup of name -> function 18 | // Also stores a description, usage is handled by the command itself 19 | typedef struct { 20 | const char *name; 21 | const char *description; 22 | void (*print_usage)(); 23 | int (*func)(const int argc, const char *argv[]); 24 | } rb_cli_cmd; 25 | 26 | // Individual commands are taped together using CMake's build system 27 | // so that all a command needs to do is globally define the struct 28 | // and it will be added to the rb_cli_commands array at build time 29 | // 30 | // IMPORTANT: The struct name MUST be the same as the file name 31 | // for the build system to properly add it to the array. 32 | extern rb_cli_cmd *rb_cli_cmds[]; 33 | 34 | #endif // CLI_MODULE_H 35 | 36 | -------------------------------------------------------------------------------- /src/librootbeer/src/api/execute_command.c: -------------------------------------------------------------------------------- 1 | #include "rb_rootbeer.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int rb_execute_command(rb_lua_t *ctx, const char *command, const char *args) { 9 | // Execute with posix_spawn 10 | pid_t pid; 11 | int status; 12 | extern char **environ; 13 | 14 | int execstat = posix_spawn( 15 | &pid, 16 | command, 17 | NULL, // No file actions 18 | NULL, // No spawn attributes 19 | (char *const[]){(char *)command, (char *)args, NULL}, // Arguments 20 | environ // Environment variables 21 | ); 22 | 23 | if (execstat != 0) { 24 | printf("Error executing command '%s': %s\n", command, strerror(execstat)); 25 | return -1; 26 | } 27 | 28 | // Wait for the command to finish 29 | int wait_status; 30 | if (waitpid(pid, &wait_status, 0) == -1) { 31 | printf("Error waiting for command '%s': %s\n", command, strerror(errno)); 32 | return -1; 33 | } 34 | 35 | if (WIFEXITED(wait_status)) { 36 | status = WEXITSTATUS(wait_status); 37 | } else if (WIFSIGNALED(wait_status)) { 38 | status = WTERMSIG(wait_status); 39 | } else { 40 | status = -1; // Unknown status 41 | } 42 | 43 | printf("Command '%s' executed with status: %d\n", command, status); 44 | return status; 45 | } 46 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/rev_store/view.c: -------------------------------------------------------------------------------- 1 | #include "store_module.h" 2 | 3 | // Counts how many revisions are stored in the system. 4 | // It's a simple file count of STORE_ROOT/store 5 | int rb_store_get_revision_count() { 6 | char *store_path = malloc(strlen(STORE_ROOT) + strlen("/store") + 1); 7 | sprintf(store_path, "%s/store", STORE_ROOT); 8 | 9 | DIR *dir = opendir(store_path); 10 | if (dir == NULL) { 11 | return 0; 12 | } 13 | 14 | int count = 0; 15 | struct dirent *entry; 16 | while ((entry = readdir(dir)) != NULL) { 17 | // Ignore . and .. 18 | if (strcmp(entry->d_name, ".") == 0 19 | || strcmp(entry->d_name, "..") == 0) { 20 | continue; 21 | } 22 | 23 | if (entry->d_type == DT_DIR) { 24 | count++; 25 | } 26 | } 27 | 28 | closedir(dir); 29 | return count; 30 | } 31 | 32 | // Returns all the revisions stored in the system. 33 | rb_revision_t **rb_store_get_all(int count) { 34 | if (count == 0) { 35 | return NULL; 36 | } 37 | 38 | rb_revision_t **revs = malloc(count * sizeof(rb_revision_t *)); 39 | if (revs == NULL) { 40 | return NULL; 41 | } 42 | 43 | for (int i = 0; i < count; i++) { 44 | rb_revision_t *rev = rb_store_get_revision_by_id(i); 45 | if (rev == NULL) { 46 | continue; 47 | } 48 | 49 | revs[i] = rev; 50 | } 51 | 52 | return revs; 53 | } 54 | -------------------------------------------------------------------------------- /src/plugins/rpm_pkg/src/plugin.c: -------------------------------------------------------------------------------- 1 | #include "rb_plugin.h" 2 | #include "rpm_pkg.h" 3 | #include 4 | #include 5 | 6 | static int with_pkgs(lua_State *L) { 7 | if (!lua_istable(L, 1)) { 8 | lua_pushstring(L, "Expected a table of package names as the first argument"); 9 | lua_error(L); 10 | return 0; 11 | } 12 | 13 | // Convert into char ** array 14 | size_t len = lua_objlen(L, 1); 15 | char **pkgs = malloc((len + 1) * sizeof(char *)); 16 | if (pkgs == NULL) { 17 | lua_pushstring(L, "Memory allocation failed"); 18 | lua_error(L); 19 | return 0; 20 | } 21 | 22 | for (size_t i = 0; i < len; i++) { 23 | lua_rawgeti(L, 1, i + 1); // Lua is 1-indexed 24 | if (!lua_isstring(L, -1)) { 25 | free(pkgs); 26 | lua_pushstring(L, "Expected all elements in the table to be strings"); 27 | lua_error(L); 28 | return 0; 29 | } 30 | 31 | pkgs[i] = strdup(lua_tostring(L, -1)); 32 | lua_pop(L, 1); // Pop the string from the stack 33 | printf("Package %zu: %s\n", i + 1, pkgs[i]); 34 | } 35 | 36 | query_dnf_packages(pkgs, len); 37 | return 0; 38 | } 39 | 40 | static const luaL_Reg functions[] = { 41 | {"with_pkgs", with_pkgs}, 42 | {NULL, NULL} 43 | }; 44 | 45 | RB_PLUGIN( 46 | rpm_pkg, 47 | "Deterministically manage packaging on RPM-based systems", 48 | "0.0.1", 49 | functions 50 | ) 51 | -------------------------------------------------------------------------------- /src/plugins/rootbeer_core/src/register_module.c: -------------------------------------------------------------------------------- 1 | #include "lua.h" 2 | #include "rootbeer_core.h" 3 | 4 | static int rb_lua_return_upvalue(lua_State *L) { 5 | lua_pushvalue(L, lua_upvalueindex(1)); 6 | return 1; 7 | } 8 | 9 | // TODO: Support adding existing modules by coalescing with the existing table 10 | int rb_core_register_module(lua_State *L) { 11 | const char *modname = luaL_checkstring(L, 1); 12 | luaL_checktype(L, 2, LUA_TTABLE); 13 | if (modname == NULL || modname[0] == '\0') { 14 | luaL_error(L, "Module name cannot be empty"); 15 | return 0; 16 | } 17 | 18 | // We need to check if "rootbeer." already exists 19 | 20 | lua_getglobal(L, "package"); 21 | lua_getfield(L, -1, "preload"); 22 | 23 | char fullmodname[256]; 24 | snprintf(fullmodname, sizeof(fullmodname), "rootbeer.%s", modname); 25 | 26 | lua_getfield(L, -1, fullmodname); 27 | if (!lua_isnil(L, -1)) { 28 | lua_pop(L, 3); // pop preload table, package table, and existing module 29 | return luaL_error(L, "Module '%s' already exists", fullmodname); 30 | } 31 | 32 | // Create a new module table 33 | lua_pop(L, 1); // pop nil value for existing module 34 | 35 | // preload[fullmodname] = function() return end 36 | lua_pushstring(L, fullmodname); 37 | lua_pushvalue(L, 2); // the module table 38 | lua_pushcclosure(L, rb_lua_return_upvalue, 1); // closure to return the module table 39 | lua_settable(L, -3); // preload[fullmodname] = function 40 | 41 | lua_pop(L, 2); // pop preload and package 42 | return RB_OK; 43 | } 44 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'Rootbeer' 10 | copyright = '2025, Aarnav Tale' 11 | author = 'Aarnav Tale' 12 | 13 | # -- General configuration --------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 15 | 16 | extensions = [ 17 | 'breathe', 18 | 'myst_parser', 19 | 'sphinx_lua_ls', 20 | 'sphinx.ext.autodoc', 21 | 'sphinx.ext.viewcode' 22 | ] 23 | breathe_projects = {"Rootbeer": "../.doxygen/xml"} 24 | breathe_default_project = "Rootbeer" 25 | lua_ls_project_root = "../../" 26 | 27 | templates_path = ['_templates'] 28 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 29 | 30 | myst_enable_extensions = [ 31 | # "amsmath", 32 | "colon_fence", 33 | # "deflist", 34 | # "fieldlist", 35 | # "html_admonition", 36 | # "html_image", 37 | # "linkify", 38 | # "replacements", 39 | # "smartquotes", 40 | "substitution" 41 | ] 42 | 43 | myst_substitutions = { 44 | "ghdir": "https://github.com/tale/rootbeer/tree/main", 45 | } 46 | 47 | 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 51 | 52 | html_theme = 'furo' 53 | html_static_path = ['_static'] 54 | -------------------------------------------------------------------------------- /src/plugins/rpm_pkg/src/query.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "rpm_pkg.h" 7 | 8 | void query_dnf_packages(char **packages, size_t count) { 9 | Pool *pool = load_all_solv_repos(DNF5_CACHE); 10 | Queue job; 11 | queue_init(&job); 12 | 13 | for (size_t i = 0; i < count; i++) { 14 | const char *pkg = packages[i]; 15 | if (!pkg || pkg[0] == '\0') { 16 | continue; 17 | } 18 | 19 | Id name = pool_str2id(pool, pkg, 1); 20 | if (!name) { 21 | fprintf(stderr, "Package '%s' not found in the pool.\n", pkg); 22 | continue; 23 | } 24 | 25 | queue_push2(&job, SOLVER_INSTALL | SOLVER_SOLVABLE_NAME, name); 26 | } 27 | 28 | Solver *solver = solver_create(pool); 29 | solver_solve(solver, &job); 30 | Transaction *t = solver_create_transaction(solver); 31 | if (!t) { 32 | fprintf(stderr, "Failed to create transaction.\n"); 33 | solver_free(solver); 34 | pool_free(pool); 35 | return; 36 | } 37 | 38 | Queue installs; 39 | queue_init(&installs); 40 | transaction_installedresult(t, &installs); 41 | 42 | for (int i = 0; i < installs.count; i++) { 43 | Id id = installs.elements[i]; 44 | Solvable *s = pool_id2solvable(pool, id); 45 | 46 | if (s) { 47 | printf("Package: %s\n", pool_id2str(pool, s->name)); 48 | printf("Version: %s\n", pool_id2str(pool, s->evr)); 49 | printf("Arch: %s\n", pool_id2str(pool, s->arch)); 50 | } else { 51 | fprintf(stderr, "Solvable for ID %d not found.\n", id); 52 | } 53 | } 54 | 55 | queue_free(&installs); 56 | solver_free(solver); 57 | pool_free(pool); 58 | queue_free(&job); 59 | } 60 | -------------------------------------------------------------------------------- /src/librootbeer/src/helpers/strlist.c: -------------------------------------------------------------------------------- 1 | #include "rb_strlist.h" 2 | #include 3 | #include 4 | 5 | int rb_strlist_init(rb_strlist_t *list, size_t initial_capacity) { 6 | list->count = 0; 7 | list->capacity = initial_capacity; 8 | list->items = malloc(initial_capacity * sizeof(char *)); 9 | if (!list->items) { 10 | return -1; 11 | } 12 | 13 | return 0; 14 | } 15 | 16 | /** 17 | * Resize the string list to a new capacity. 18 | * 19 | * @param list Pointer to the string list. 20 | * @return 0 on success, -1 on failure. 21 | */ 22 | static int rb_strlist_resize(rb_strlist_t *list) { 23 | // 4 is a safe default capacity for small lists. 24 | size_t new_capacity = list->capacity == 0 ? 4 : list->capacity * 2; 25 | char **new_list = realloc(list->items, new_capacity * sizeof(char *)); 26 | if (!new_list) { 27 | return -1; 28 | } 29 | 30 | list->items = new_list; 31 | list->capacity = new_capacity; 32 | return 0; 33 | } 34 | 35 | int rb_strlist_add(rb_strlist_t *list, const char *str) { 36 | for (size_t i = 0; i < list->count; i++) { 37 | if (strcmp(list->items[i], str) == 0) { 38 | return 0; 39 | } 40 | } 41 | 42 | if (list->count >= list->capacity) { 43 | if (rb_strlist_resize(list) < 0) { 44 | return -1; 45 | } 46 | } 47 | 48 | char *copy = strdup(str); 49 | if (!copy) { 50 | return -1; 51 | } 52 | 53 | list->items[list->count++] = copy; 54 | return 0; 55 | } 56 | 57 | void rb_strlist_free(rb_strlist_t *list) { 58 | if (list == NULL) { 59 | return; 60 | } 61 | 62 | if (list->items) { 63 | for (size_t i = 0; i < list->count; i++) { 64 | free(list->items[i]); 65 | } 66 | 67 | free(list->items); 68 | list->items = NULL; 69 | } 70 | 71 | list->count = 0; 72 | list->capacity = 0; 73 | } 74 | -------------------------------------------------------------------------------- /src/rootbeer_cli/include/rb_ctx_state.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rb_ctx_state.h 3 | * @brief Runtime implementation details to manage the Rootbeer context. 4 | * 5 | * This file complements @ref rb_ctx.h by defining the functions only needed 6 | * by the runtime implementation of Rootbeer (the CLI). It has all of the lower 7 | * level details like dynamically allocating the context and converting it 8 | * to the data required to deterministically generate a revision. 9 | */ 10 | #ifndef RB_CTX_STATE_H 11 | #define RB_CTX_STATE_H 12 | 13 | #include 14 | 15 | /** 16 | * @brief Initial capacity for tracked imported Lua modules. 17 | */ 18 | #define RB_INIT_LUAMODULES_CAP 8 19 | 20 | /** 21 | * @brief Initial capacity for tracked static inputs. 22 | */ 23 | #define RB_INIT_STATICINPUTS_CAP 16 24 | 25 | /** 26 | * @brief Initial capacity for intermediate plugin outputs. 27 | */ 28 | #define RB_INIT_INTERMEDIATES_CAP 8 29 | 30 | /** 31 | * @brief Initial capacity for tracked generated outputs. 32 | */ 33 | #define RB_INIT_GENERATED_CAP 16 34 | 35 | /** 36 | * Initializes the Rootbeer context. 37 | * This function initializes a blank Rootbeer context structure. 38 | * Because context is available as a light userdata in Lua, it needs to be 39 | * dynamically allocated along with its members. 40 | * 41 | * @return Pointer to the newly allocated Rootbeer context. 42 | */ 43 | rb_ctx_t *rb_ctx_init(void); 44 | 45 | /** 46 | * Frees the Rootbeer context. 47 | * This function frees the Rootbeer context structure and all of its members. 48 | * It is used to clean up resources when the context is no longer needed. 49 | * 50 | * @param rb_ctx Pointer to the Rootbeer context to be freed. 51 | */ 52 | void rb_ctx_free(rb_ctx_t *rb_ctx); 53 | 54 | #endif // RB_CTX_STATE_H 55 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": [ 6 | "nixpkgs" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1749398372, 11 | "narHash": "sha256-tYBdgS56eXYaWVW3fsnPQ/nFlgWi/Z2Ymhyu21zVM98=", 12 | "owner": "hercules-ci", 13 | "repo": "flake-parts", 14 | "rev": "9305fe4e5c2a6fcf5ba6a3ff155720fbe4076569", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "hercules-ci", 19 | "repo": "flake-parts", 20 | "type": "github" 21 | } 22 | }, 23 | "nixpkgs": { 24 | "locked": { 25 | "lastModified": 1750506804, 26 | "narHash": "sha256-VLFNc4egNjovYVxDGyBYTrvVCgDYgENp5bVi9fPTDYc=", 27 | "owner": "NixOS", 28 | "repo": "nixpkgs", 29 | "rev": "4206c4cb56751df534751b058295ea61357bbbaa", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "NixOS", 34 | "ref": "nixos-unstable", 35 | "repo": "nixpkgs", 36 | "type": "github" 37 | } 38 | }, 39 | "root": { 40 | "inputs": { 41 | "flake-parts": "flake-parts", 42 | "nixpkgs": "nixpkgs", 43 | "systems": "systems" 44 | } 45 | }, 46 | "systems": { 47 | "locked": { 48 | "lastModified": 1681028828, 49 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 50 | "owner": "nix-systems", 51 | "repo": "default", 52 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 53 | "type": "github" 54 | }, 55 | "original": { 56 | "owner": "nix-systems", 57 | "repo": "default", 58 | "type": "github" 59 | } 60 | } 61 | }, 62 | "root": "root", 63 | "version": 7 64 | } 65 | -------------------------------------------------------------------------------- /src/rootbeer_cli/include/lua_init.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file lua_init.h 3 | * @brief Lua module initialization and context management. 4 | * 5 | * This file contains all the functions to manage the initialization of the 6 | * LuaJIT VM and the context for the Lua runtime. It includes the necessary 7 | * functions to set up the environment and support loading any rootbeer plugins. 8 | */ 9 | #ifndef LUA_INIT_H 10 | #define LUA_INIT_H 11 | 12 | #include "rb_ctx.h" 13 | #include 14 | 15 | /** 16 | * Bootstraps the LuaJIT VM and initializes the Lua context. 17 | * This function is called on `rb apply ` to set up the environment 18 | * relative to the file being run, pulling in any necessary lua libs. 19 | * 20 | * @param L Pointer to the Lua state. 21 | * @param entry_file The Lua file to be executed as the entry point. 22 | * @return 0 on success, or a non-zero error code on failure. 23 | */ 24 | int lua_runtime_init(lua_State *L, const char *entry_file); 25 | 26 | /** 27 | * A faithful hook for the Lua require function. 28 | * When we setup the Lua environment, we replace the original `require` with 29 | * this hook which allows us to track which Lua files are required before 30 | * executing the original require function. 31 | * 32 | * @param L Pointer to the Lua state. 33 | * @return 1 on success, or a Lua error on failure. 34 | */ 35 | int lua_runtime_require_hook(lua_State *L); 36 | 37 | /** 38 | * Registers the Lua context with the Lua state. 39 | * This function is used to register the context so that it can be accessed 40 | * from Lua scripts. 41 | * 42 | * @param L Pointer to the Lua state. 43 | * @param ctx Pointer to the rootbeer context. 44 | * @return 0 on success, or a non-zero error code on failure. 45 | */ 46 | int lua_register_context(lua_State *L, rb_ctx_t *ctx); 47 | 48 | #endif // LUA_INIT_H 49 | -------------------------------------------------------------------------------- /src/plugins/rootbeer_core/src/to_json.c: -------------------------------------------------------------------------------- 1 | #include "rootbeer_core.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | static void table_to_json_object(lua_State *L, cJSON *json, int index); 10 | static cJSON *lua_table_to_json(lua_State *L, int index) { 11 | if (index < 0) index = lua_gettop(L) + index + 1; 12 | cJSON *root = cJSON_CreateObject(); 13 | table_to_json_object(L, root, index); 14 | return root; 15 | } 16 | 17 | int rb_core_to_json(lua_State *L) { 18 | luaL_checktype(L, 1, LUA_TTABLE); 19 | cJSON *json = lua_table_to_json(L, 1); 20 | if (!json) { 21 | lua_pushnil(L); 22 | return 1; 23 | } 24 | 25 | char *json_string = cJSON_PrintUnformatted(json); 26 | if (!json_string) { 27 | cJSON_Delete(json); 28 | lua_pushnil(L); 29 | return 1; 30 | } 31 | 32 | lua_pushstring(L, json_string); 33 | cJSON_Delete(json); 34 | free(json_string); 35 | return 1; 36 | } 37 | 38 | static void table_to_json_object(lua_State *L, cJSON *json, int index) { 39 | lua_pushnil(L); 40 | while (lua_next(L, index) != 0) { 41 | const char *key = lua_tostring(L, -2); 42 | if (!key) continue; 43 | 44 | switch (lua_type(L, -1)) { 45 | case LUA_TSTRING: 46 | cJSON_AddStringToObject(json, key, lua_tostring(L, -1)); 47 | break; 48 | case LUA_TNUMBER: 49 | cJSON_AddNumberToObject(json, key, lua_tonumber(L, -1)); 50 | break; 51 | case LUA_TBOOLEAN: 52 | cJSON_AddBoolToObject(json, key, lua_toboolean(L, -1)); 53 | break; 54 | case LUA_TTABLE: { 55 | cJSON *sub = lua_table_to_json(L, lua_gettop(L)); 56 | cJSON_AddItemToObject(json, key, sub); 57 | break; 58 | } 59 | default: 60 | // skip unsupported types 61 | printf("Skipping unsupported type for key '%s': %s\n", key, lua_typename(L, lua_type(L, -1))); 62 | break; 63 | } 64 | lua_pop(L, 1); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/test.lua: -------------------------------------------------------------------------------- 1 | local rb = require('rootbeer') 2 | -- rb.debug_test('Hello') -- YIPPEE! 3 | 4 | -- Loop through and print the numbers 1 to 10 5 | for i = 1, 10 do 6 | print('Number: ' .. i) 7 | end 8 | 9 | -- Create a table 10 | local table = { 11 | ['key1'] = 'value1', 12 | ['key2'] = 'value2', 13 | ['key3'] = 'value3' 14 | } 15 | 16 | -- Loop through the table and print the key and value 17 | for key, value in pairs(table) do 18 | print('Key: ' .. key .. ', Value: ' .. value) 19 | end 20 | 21 | require('tale.test2') 22 | require('test3') 23 | 24 | rb.ref_file('./test.bash') 25 | 26 | -- local ok, err = pcall(function() 27 | -- assert(rb.link_file('./test.bash', './test2.bash')) 28 | -- end) 29 | 30 | -- if not ok then 31 | -- print("❌ Test failed:", err) 32 | -- else 33 | -- print("✅ link_file succeeded") 34 | -- end 35 | 36 | 37 | -- rb.ref_file('./noexist.bash') 38 | -- 39 | 40 | -- Create a random table for lua to json 41 | local json_data = rb.to_json({ 42 | test = "foo", 43 | bar = "joe" 44 | }) 45 | 46 | print("JSON Data: " .. json_data) 47 | rb.write_file("./test/test.json", json_data) 48 | 49 | -- Example of what interpolation can look like 50 | local bash_conf = { 51 | env = { 52 | FOO = "bar", 53 | BAZ = "qux" 54 | }, 55 | 56 | aliases = { 57 | alias1 = "echo 'This is alias 1'", 58 | alias2 = "echo 'This is alias 2'" 59 | } 60 | } 61 | 62 | -- Creates a .bashrc 63 | function create_bash_config(config) 64 | local bashrc = "" 65 | 66 | -- Add environment variables 67 | if config.env then 68 | for key, value in pairs(config.env) do 69 | bashrc = bashrc .. "export " .. key .. "=\"" .. value .. "\"\n" 70 | end 71 | end 72 | 73 | -- Add aliases 74 | if config.aliases then 75 | for alias, command in pairs(config.aliases) do 76 | bashrc = bashrc .. "alias " .. alias .. "=\"" .. command .. "\"\n" 77 | end 78 | end 79 | 80 | return bashrc 81 | end 82 | 83 | local bash_config = rb.interpolate_table(bash_conf, create_bash_config) 84 | rb.write_file("./test/.bashrc.test", bash_config) 85 | -------------------------------------------------------------------------------- /src/internal_include/rb_strlist.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rb_strlist.h 3 | * @brief Contains the definition of the rb_strlist_t structure. 4 | * 5 | * General purpose string list structure used in Rootbeer. 6 | * This header defines the structure along with helper methods for managing 7 | * the `rb_strlist_t` type. 8 | */ 9 | #ifndef RB_STRLIST_H 10 | #define RB_STRLIST_H 11 | 12 | #include 13 | 14 | /** 15 | * @brief A structure to hold a list of strings. 16 | * This structure is used to manage a list of strings that is reused for the 17 | * context storing any tracked files, such as Lua scripts, extra files, etc. 18 | * The list is capable of self-expanding to accommodate more strings as needed. 19 | */ 20 | typedef struct { 21 | char **items; //!< Array of strings. 22 | size_t count; //!< Number of strings in the list. 23 | size_t capacity; //!< Capacity of the array, to avoid reallocating too much. 24 | } rb_strlist_t; 25 | 26 | /** 27 | * Initializes a string list with a specified initial capacity. 28 | * 29 | * @param list Pointer to the rb_strlist_t structure to initialize. 30 | * @param initial_capacity The initial capacity of the string list. 31 | * @return 0 on success, or a non-zero error code on failure. 32 | */ 33 | int rb_strlist_init(rb_strlist_t *list, size_t initial_capacity); 34 | 35 | /** 36 | * Adds a new string to the string list. 37 | * If the list is full, it will automatically expand its capacity. 38 | * IMPORTANT: This uses linear O(n) scanning for deduplication. 39 | * 40 | * @param list Pointer to the rb_strlist_t structure. 41 | * @param str The string to add to the list. 42 | * @return 0 on success, or a non-zero error code on failure. 43 | */ 44 | int rb_strlist_add(rb_strlist_t *list, const char *str); 45 | 46 | /** 47 | * Frees the memory allocated for the string list. 48 | * IMPORTANT: This will also free all strings stored in the list. 49 | * 50 | * @param list Pointer to the rb_strlist_t structure to free. 51 | */ 52 | void rb_strlist_free(rb_strlist_t *list); 53 | 54 | #endif // RB_STRLIST_H 55 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/rev_store/manage.c: -------------------------------------------------------------------------------- 1 | #include "store_module.h" 2 | 3 | // Creates the directory structure for the store. 4 | // This is a simple directory structure with two directories: 5 | // - store: contains the revisions of the store 6 | // - _gen: contains the generated files 7 | // 8 | // This function will fail if the store already exists. 9 | void rb_store_init_or_die() { 10 | char *store_gen_path = malloc(strlen(STORE_ROOT) + strlen("_gen") + 2); 11 | sprintf(store_gen_path, "%s/%s", STORE_ROOT, "_gen"); 12 | 13 | char *store_rev_path = malloc(strlen(STORE_ROOT) + strlen("store") + 2); 14 | sprintf(store_rev_path, "%s/%s", STORE_ROOT, "store"); 15 | 16 | if (access(store_rev_path, F_OK | R_OK) == 0) { 17 | printf("error: store already exists and is probably initialized\n"); 18 | exit(1); 19 | } 20 | 21 | int uperm = getuid(); 22 | if (uperm != 0) { 23 | printf("error: must run as root to initialize store\n"); 24 | exit(1); 25 | } 26 | 27 | // We don't have a "mkdir_p" so we need to do this manually 28 | // But we will sanely assume that /opt exists on the system 29 | if (mkdir(STORE_ROOT, 0755) != 0) { 30 | printf("error: could not create store directory\n"); 31 | exit(1); 32 | } 33 | 34 | if (mkdir(store_rev_path, 0755) != 0) { 35 | printf("error: could not create store rev directory\n"); 36 | exit(1); 37 | } 38 | 39 | if (mkdir(store_gen_path, 0755) != 0) { 40 | printf("error: could not create store gen directory\n"); 41 | exit(1); 42 | } 43 | } 44 | 45 | // Currently broken teardown function. 46 | // This function will fail if the store does not exist. 47 | void rb_store_destroy() { 48 | if (access(STORE_ROOT, F_OK | R_OK) != 0) { 49 | printf("error: store does not exist\n"); 50 | exit(1); 51 | } 52 | 53 | int uperm = getuid(); 54 | if (uperm != 0) { 55 | printf("error: must run as root to destroy store\n"); 56 | exit(1); 57 | } 58 | 59 | // Yeah this does NOT work because I haven't recursed yet 60 | // TODO: Recurse and delete all files and directories *sigh* 61 | if (rmdir(STORE_ROOT) != 0) { 62 | printf("error: could not destroy store directory\n"); 63 | exit(1); 64 | } 65 | 66 | printf("store destroyed\n"); 67 | } 68 | -------------------------------------------------------------------------------- /src/plugins/homebrew/src/core.c: -------------------------------------------------------------------------------- 1 | // #include "rootbeer_core.h" 2 | #include "rb_rootbeer.h" 3 | #include "rb_plugin.h" 4 | 5 | int rb_brew(lua_State *L) { 6 | // Check if the first argument is a string 7 | if (!lua_isstring(L, 1)) { 8 | lua_pushstring(L, "Expected a string as the first argument"); 9 | lua_error(L); 10 | return 0; // This line will never be reached due to lua_error 11 | } 12 | 13 | // Setuid to effective user ID since brew cannot run as root 14 | // and it is user specific. 15 | const char *user_uid = getenv("SUDO_UID"); 16 | const char *user_gid = getenv("SUDO_GID"); 17 | if (user_uid && user_gid) { 18 | uid_t uid = (uid_t)atoi(user_uid); 19 | gid_t gid = (gid_t)atoi(user_gid); 20 | 21 | setgid(gid); 22 | setuid(uid); 23 | } 24 | 25 | const char *brew_name = lua_tostring(L, 1); 26 | char **args = malloc(3 * sizeof(char *)); 27 | if (args == NULL) { 28 | lua_pushstring(L, "Memory allocation failed"); 29 | lua_error(L); 30 | return 0; // This line will never be reached due to lua_error 31 | } 32 | 33 | args[0] = strdup("/opt/homebrew/bin/brew"); 34 | if (args[0] == NULL) { 35 | free(args); 36 | lua_pushstring(L, "Memory allocation failed"); 37 | lua_error(L); 38 | return 0; // This line will never be reached due to lua_error 39 | } 40 | 41 | args[1] = strdup(brew_name); 42 | if (args[1] == NULL) { 43 | free(args[0]); 44 | free(args); 45 | lua_pushstring(L, "Memory allocation failed"); 46 | lua_error(L); 47 | return 0; // This line will never be reached due to lua_error 48 | } 49 | 50 | args[2] = NULL; // Null-terminate the array of arguments 51 | 52 | int status = rb_execute_command(NULL, "/opt/homebrew/bin/brew", args); 53 | if (status != 0) { 54 | lua_pushstring(L, "Failed to execute brew command"); 55 | lua_error(L); 56 | return 0; // This line will never be reached due to lua_error 57 | } 58 | 59 | setuid(0); // Reset to root user after executing brew command 60 | setgid(0); // Reset to root group after executing brew command 61 | 62 | return 0; 63 | } 64 | 65 | static const luaL_Reg functions[] = { 66 | {"brew", rb_brew}, 67 | {NULL, NULL} 68 | }; 69 | 70 | RB_PLUGIN(brew, "brew plugin", "1.0.0", functions) 71 | -------------------------------------------------------------------------------- /guix.scm: -------------------------------------------------------------------------------- 1 | (use-modules (guix gexp) 2 | (guix packages) 3 | (guix git-download) 4 | (guix build-system meson) 5 | (guix build-system cmake) 6 | (gnu packages pkg-config) 7 | (gnu packages compression) 8 | (gnu packages javascript) 9 | (gnu packages lua) 10 | ((guix licenses) 11 | #:prefix license:)) 12 | 13 | (define libsolv 14 | (package 15 | (name "libsolv") 16 | (version "0.7.34") 17 | (source 18 | (origin 19 | (method git-fetch) 20 | (uri (git-reference 21 | (url "https://github.com/openSUSE/libsolv.git") 22 | (commit version))) 23 | (file-name (git-file-name name version)) 24 | (sha256 25 | (base32 "183vahb5fmkci9vz63wbram051mv7m1ralq3gqr70fizv2p4bx87")))) 26 | (build-system cmake-build-system) 27 | (inputs (list zlib)) 28 | (synopsis #f) 29 | (description #f) 30 | (home-page "https://github.com/openSUSE/libsolv") 31 | (license license:bsd-3))) 32 | 33 | (package 34 | (name "rootbeer") 35 | (version "git") 36 | (source 37 | (local-file (dirname (current-filename)) 38 | #:recursive? #t)) 39 | 40 | (build-system meson-build-system) 41 | (arguments 42 | (list 43 | #:phases 44 | #~(modify-phases %standard-phases 45 | (replace 'install 46 | (lambda* (#:key outputs #:allow-other-keys) 47 | (let* ((out (assoc-ref outputs "out")) 48 | (out-bin (string-append out "/bin")) 49 | (out-lib (string-append out "/lib/rootbeer"))) 50 | (mkdir-p out-bin) 51 | (mkdir-p out-lib) 52 | 53 | (copy-file "src/rootbeer_cli/rb" 54 | (string-append out-bin "/rb")) 55 | 56 | (copy-file "src/librootbeer/librootbeer.a" 57 | (string-append out-lib "/librootbeer.a")) 58 | 59 | #t)))))) 60 | (inputs (list libsolv cjson luajit)) 61 | (native-inputs (list pkg-config)) 62 | (synopsis #f) 63 | (description #f) 64 | (home-page "https://tale.github.io/rootbeer/") 65 | (license license:expat)) 66 | -------------------------------------------------------------------------------- /src/plugins/rpm_pkg/src/solv/repo_loader.c: -------------------------------------------------------------------------------- 1 | #include "rpm_pkg.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | static void populate_solv_files(const char *dir, char **found, size_t *count) { 11 | DIR *d = opendir(dir); 12 | if (!d) { 13 | return; 14 | } 15 | 16 | struct dirent *ent; 17 | while ((ent = readdir(d)) != NULL) { 18 | // Skip the usual troll stuff 19 | if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { 20 | continue; 21 | } 22 | 23 | char path[PATH_MAX]; 24 | snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name); 25 | 26 | struct stat st; 27 | if (stat(path, &st) != 0) { 28 | continue; 29 | } 30 | 31 | // Recursing down the tree to a .solv file. I'm not sure how reliable 32 | // this is if there are duplicates BUTTTTTT lets pray for now. 33 | if (S_ISDIR(st.st_mode)) { 34 | populate_solv_files(path, found, count); 35 | } else if (S_ISREG(st.st_mode) && strstr(ent->d_name, ".solv") != NULL) { 36 | if (*count < MAX_REPOS) { 37 | found[*count] = strdup(path); 38 | (*count)++; 39 | } 40 | } 41 | } 42 | 43 | closedir(d); 44 | } 45 | 46 | 47 | Pool *load_all_solv_repos(const char *solv_root) { 48 | Pool *pool = pool_create(); 49 | pool_setdisttype(pool, DISTTYPE_RPM); 50 | 51 | char *solv_paths[MAX_REPOS]; 52 | size_t repos_count = 0; 53 | populate_solv_files(solv_root, solv_paths, &repos_count); 54 | if (repos_count == 0) { 55 | pool_free(pool); 56 | return NULL; // No solv files found 57 | } 58 | 59 | for (size_t i = 0; i < repos_count; i++) { 60 | if (solv_paths[i] == NULL) { 61 | continue; 62 | } 63 | 64 | FILE *fp = fopen(solv_paths[i], "r"); 65 | if (fp == NULL) { 66 | free(solv_paths[i]); 67 | continue; 68 | } 69 | 70 | const char *basename = strrchr(solv_paths[i], '/'); 71 | Repo *repo = repo_create(pool, basename ? basename + 1 : solv_paths[i]); 72 | if (repo_add_solv(repo, fp, 0) != 0) { 73 | fprintf(stderr, "Failed to add solv file: %s\n", solv_paths[i]); 74 | } else { 75 | repo_internalize(repo); 76 | } 77 | 78 | fclose(fp); 79 | free(solv_paths[i]); 80 | } 81 | 82 | pool_createwhatprovides(pool); 83 | return pool; 84 | } 85 | -------------------------------------------------------------------------------- /include/rb_plugin.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rb_plugin.h 3 | * 4 | * This header defines the structure and macros for Rootbeer plugins. 5 | * Using the @ref RB_PLUGIN macro, developers can easily create plugins 6 | * for Rootbeer, without having to deal with boilerplate code. 7 | * See \ghdir{src/plugins/rootbeer_core} for an example of a plugin. 8 | */ 9 | #ifndef RB_PLUGIN_H 10 | #define RB_PLUGIN_H 11 | 12 | #include 13 | 14 | /** 15 | * The internal structure for a Rootbeer plugin. 16 | * This structure defines a plugin's metadata and the functions 17 | * it provides to the Lua environment. Internally, the CLI will parse 18 | * this structure to make the plugin available in the Lua environment. 19 | * 20 | * Instead of manually defining this, use @ref RB_PLUGIN to define a plugin. 21 | */ 22 | typedef struct { 23 | const char *plugin_name; //!< Registers a `rootbeer.` module in Lua. 24 | const char *description; //!< Description of the plugin for the CLI. 25 | const char *version; //!< Version of the plugin, used for CLI and Lua. 26 | const luaL_Reg *functions; //!< Array of functions to be registered in Lua. 27 | int (*entrypoint)(lua_State *L); //!< Generates a function for the package.preload table. 28 | } rb_plugin_t; 29 | 30 | 31 | /** 32 | * @def RB_PLUGIN 33 | * Macro to define a Rootbeer plugin. 34 | * It will automatically generate the necessary entrypoint function 35 | * and globals so that the rootbeer CLI can recognize the plugin and load it. 36 | * 37 | * This macro simplifies the process of defining a plugin by automatically 38 | * generating the necessary entrypoint function and populating the plugin 39 | * structure with the provided name, description, version, and functions. 40 | * 41 | * @param name The name of the plugin, used in Lua as `rootbeer.`. 42 | * @param desc A short description of the plugin for CLI help. 43 | * @param ver The version of the plugin, used for CLI and Lua. 44 | * @param f An array of functions to be registered in Lua. 45 | */ 46 | #define RB_PLUGIN(name, desc, ver, f) \ 47 | int lua_mod_entrypoint_##name(lua_State *L) { \ 48 | luaL_newlib(L, f); \ 49 | return 1; \ 50 | } \ 51 | const rb_plugin_t rb_plugin_##name = { \ 52 | .plugin_name = #name, \ 53 | .description = desc, \ 54 | .version = ver, \ 55 | .functions = f, \ 56 | .entrypoint = lua_mod_entrypoint_##name \ 57 | }; \ 58 | 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /src/plugins/rootbeer_core/src/link_file.c: -------------------------------------------------------------------------------- 1 | #include "rb_rootbeer.h" 2 | #include "rootbeer_core.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "rb_ctx.h" 10 | 11 | // TODO: Make this a real header :skull: 12 | int rb_create_dir(char *path); 13 | 14 | int rb_core_link_file(lua_State *L) { 15 | const char *from = luaL_checkstring(L, 1); 16 | const char *to = luaL_checkstring(L, 2); 17 | 18 | if (from == NULL || to == NULL) { 19 | return luaL_error(L, "Invalid arguments: 'from' and 'to' must be non-null strings."); 20 | } 21 | 22 | rb_ctx_t *ctx = rb_ctx_from_lua(L); 23 | 24 | // Need to resolve the realpath of the 'from' and 'to' paths. 25 | char filename_from[strlen(ctx->script_dir) + strlen(from) + 2]; 26 | snprintf(filename_from, sizeof(filename_from), "%s/%s", ctx->script_dir, from); 27 | 28 | char filename_to[strlen(ctx->script_dir) + strlen(to) + 2]; 29 | snprintf(filename_to, sizeof(filename_to), "%s/%s", ctx->script_dir, to); 30 | 31 | char resolved_from[PATH_MAX]; 32 | if (realpath(filename_from, resolved_from) == NULL) { 33 | return luaL_error(L, "Failed to resolve 'from' path '%s': %s", filename_from, strerror(errno)); 34 | } 35 | 36 | if (access(resolved_from, F_OK | R_OK) != 0) { 37 | return luaL_error(L, "Cannot access 'from' file '%s': %s", resolved_from, strerror(errno)); 38 | } 39 | 40 | if (access(filename_to, F_OK) == 0) { 41 | // If the 'to' file already exists, we cannot link to it. 42 | return luaL_error(L, "Cannot link to '%s': file already exists.", filename_to); 43 | } 44 | 45 | 46 | // If we can resolve the files, symlink softlink them 47 | int status = rb_track_ref_file(ctx, from); 48 | if (status != 0) { 49 | // TODO: rb_strerror needs to exist 50 | return luaL_error(L, "Failed to track file '%s': %d", from, status); 51 | // return luaL_error(L, "Failed to track file '%s': %s", from, rb_strerror(status)); 52 | } 53 | 54 | 55 | char *parent_dir = dirname(filename_to); 56 | if (rb_create_dir(parent_dir) != 0) { 57 | return luaL_error(L, "Failed to create directory for '%s'.", parent_dir); 58 | } 59 | 60 | if (symlink(resolved_from, filename_to) != 0) { 61 | return luaL_error(L, "Failed to create symlink from '%s' to '%s': %s", from, to, strerror(errno)); 62 | } 63 | 64 | // We don't need to track the symlink itself, as it will be resolved at runtime. 65 | // So now we are done. 66 | lua_pushboolean(L, 1); 67 | return 1; 68 | } 69 | -------------------------------------------------------------------------------- /src/librootbeer/src/api/intermediate.c: -------------------------------------------------------------------------------- 1 | #include "rb_rootbeer.h" 2 | #include "rb_helpers.h" 3 | #include "rb_idlist.h" 4 | #include "rb_ctx.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | static int rb_valid_intermediate_id(const char *id) { 15 | if (id == NULL || *id == '\0') { 16 | return 0; 17 | } 18 | 19 | if (strlen(id) > RB_MAX_INTERMEDIATE_ID_LENGTH) { 20 | return 0; 21 | } 22 | 23 | for (const char *p = id; *p; p++) { 24 | if (!( 25 | (*p >= 'a' && *p <= 'z') || 26 | (*p >= 'A' && *p <= 'Z') || 27 | (*p >= '0' && *p <= '9') || 28 | *p == '_' || *p == '-' || *p == '.' 29 | )) { 30 | return 0; 31 | } 32 | } 33 | 34 | return 1; 35 | } 36 | 37 | FILE *rb_open_intermediate(rb_ctx_t *ctx, const char *id) { 38 | if (!rb_valid_intermediate_id(id)) { 39 | fprintf(stderr, "Invalid intermediate ID: %s\n", id); 40 | return NULL; 41 | } 42 | 43 | // TODO: Move this somewhere where it isn't constantly created 44 | char tmp_dir[PATH_MAX]; 45 | snprintf(tmp_dir, sizeof(tmp_dir), "%s/.rb-tmp", ctx->script_dir); 46 | if (mkdir(tmp_dir, 0755) == -1 && errno != EEXIST) { 47 | perror("Failed to create temporary directory"); 48 | return NULL; 49 | } 50 | 51 | char filename[PATH_MAX]; 52 | snprintf(filename, sizeof(filename), "%s/rb_transform_%s", tmp_dir, id); 53 | int fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0644); 54 | if (fd < 0) { 55 | perror("Failed to open intermediate file"); 56 | return NULL; 57 | } 58 | 59 | char *abs_path = realpath(filename, NULL); 60 | if (abs_path == NULL) { 61 | perror("Failed to resolve absolute path for intermediate file"); 62 | close(fd); 63 | return NULL; 64 | } 65 | 66 | char *rel_path = rb_canon_relative(ctx, abs_path); 67 | free(abs_path); 68 | if (rel_path == NULL) { 69 | close(fd); 70 | return NULL; 71 | } 72 | 73 | if (rb_idlist_add(&ctx->intermediates, id, rel_path) < 0) { 74 | free(rel_path); 75 | close(fd); 76 | return NULL; 77 | } 78 | 79 | free(rel_path); 80 | FILE *fp = fdopen(fd, "w+"); 81 | if (fp == NULL) { 82 | close(fd); 83 | return NULL; 84 | } 85 | 86 | return fp; 87 | } 88 | 89 | const char *rb_get_intermediate(rb_ctx_t *ctx, const char *id) { 90 | if (!rb_valid_intermediate_id(id)) { 91 | fprintf(stderr, "Invalid intermediate ID: %s\n", id); 92 | return NULL; 93 | } 94 | 95 | return rb_idlist_get(&ctx->intermediates, id); 96 | } 97 | -------------------------------------------------------------------------------- /src/internal_include/rb_idlist.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rb_idlist.h 3 | * @brief Contains the definition of the `rb_idlist_t` structure 4 | * 5 | * General purpose list that associates IDs with paths. 6 | * This header defines the structure along with helper methods for managing 7 | * the `rb_idlist_t` type. 8 | */ 9 | #ifndef RB_IDLIST_H 10 | #define RB_IDLIST_H 11 | 12 | #include 13 | 14 | /** 15 | * @brief A structure to hold a list of paths associated to IDs. 16 | * This structure is used to associate intermediate files to IDs, allowing 17 | * them to later be referenced and retrieved efficiently. 18 | * This list is also capable of self-expansion to accommodate more entries. 19 | */ 20 | typedef struct { 21 | char **ids; //!< Array of strings (IDs). 22 | char **paths; //!< Array of paths. 23 | size_t count; //!< Number of strings in the list. 24 | size_t capacity; //!< Capacity of the array, to avoid reallocating too much. 25 | } rb_idlist_t; 26 | 27 | /** 28 | * Initializes an ID list with a specified initial capacity. 29 | * 30 | * @param list Pointer to the rb_idlist_t structure to initialize. 31 | * @param initial_capacity The initial capacity of the ID list. 32 | * @return 0 on success, or a non-zero error code on failure. 33 | */ 34 | int rb_idlist_init(rb_idlist_t *list, size_t initial_capacity); 35 | 36 | /** 37 | * Adds a new ID and path pair to the list. 38 | * If the list is full, it will automatically expand its capacity. 39 | * IMPORTANT: This uses linear O(n) scanning for deduplication of IDs. 40 | * 41 | * @param list Pointer to the rb_idlist_t structure. 42 | * @param id The ID to add to the list. 43 | * @param str The string to add to the list. 44 | * @return 0 on success, or a non-zero error code on failure. 45 | */ 46 | int rb_idlist_add(rb_idlist_t *list, const char *id, const char *path); 47 | 48 | /** 49 | * Retrieves the path associated with a given ID. 50 | * If the ID is not found, it returns NULL. 51 | * IMPORTANT: This uses linear O(n) scanning for ID lookup. 52 | * 53 | * @param list Pointer to the rb_idlist_t structure. 54 | * @param id The ID to look up. 55 | * @return The associated path if found, or NULL if not found. 56 | */ 57 | const char *rb_idlist_get(const rb_idlist_t *list, const char *id); 58 | 59 | /** 60 | * Frees the memory allocated for the ID list. 61 | * IMPORTANT: This will also free all strings and IDs stored in the list. 62 | * 63 | * @param list Pointer to the rb_idlist_t structure to free. 64 | */ 65 | void rb_idlist_free(rb_idlist_t *list); 66 | 67 | #endif // RB_IDLIST_H 68 | -------------------------------------------------------------------------------- /src/rootbeer_cli/include/store_module.h: -------------------------------------------------------------------------------- 1 | #ifndef STORE_MODULE_H 2 | #define STORE_MODULE_H 3 | 4 | #include "rb_ctx.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define STORE_ROOT "/opt/rootbeer" 16 | 17 | // This struct is basically what an entire revision looks like 18 | // in our store. As a basic explanation, we say that a revision 19 | // contains some basic metadata, config data, and all of the 20 | // "side effects" that are generated by the revision, such as 21 | // IO related things like external files. 22 | typedef struct { 23 | int id; // Revisions count up from 0 24 | char *name; // Name of the revision (optional) 25 | time_t timestamp; // Unix timestamp 26 | char *pwd; // PWD for the config and reference files 27 | 28 | char **cfg_filesv; // Array of config file paths 29 | int cfg_filesc; // Number of config files 30 | 31 | char **ref_filesv; // Array of reference file paths 32 | int ref_filesc; // Number of reference files 33 | 34 | char **gen_filesv; 35 | int gen_filesc; 36 | } rb_revision_t; 37 | 38 | // The way we actually store revision data is like so: 39 | // STORE_ROOT/store/__ 40 | // - cfg: stores all the lua-config files used to generate the revision 41 | // - ref: stores all the reference files used to generate the revision 42 | // - _meta: contains all the metadata in a newline separated file 43 | // 44 | // Some side things: 45 | // - STORE_ROOT/_current: contains the number of the current revision 46 | // - STORE_ROOT/_gen: generated files from the current revision 47 | 48 | // Operations that our store needs to support: 49 | // - Get the current revision 50 | // - Set the current revision 51 | // - Get a revision through ID 52 | // - Get all revisions as an array 53 | // - Get the ID for a new revision 54 | // 55 | // - Initialize the store and create necessary structure 56 | // - Destroy the store and remove all data (who knows) 57 | // - Given a revision, save it to the store 58 | 59 | rb_revision_t *rb_store_get_current_revision(); 60 | rb_revision_t *rb_store_get_revision_by_id(const int id); 61 | rb_revision_t **rb_store_get_all(int count); 62 | 63 | int rb_store_set_current_revision(const int id); 64 | int rb_store_get_revision_count(); 65 | int rb_store_next_id(); 66 | 67 | void rb_store_init_or_die(); 68 | void rb_store_destroy(); 69 | 70 | int rb_store_dump_revision(rb_ctx_t *ctx); 71 | 72 | #endif // STORE_MODULE_H 73 | -------------------------------------------------------------------------------- /src/plugins/rootbeer_core/src/write_file.c: -------------------------------------------------------------------------------- 1 | #include "rb_rootbeer.h" 2 | #include "rootbeer_core.h" 3 | #include "rb_ctx.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | char *rb_resolve_full_path(lua_State *L, const char *path) { 11 | // This function should resolve the full path based on the current working directory 12 | // and any other context-specific logic. For simplicity, we assume it returns the same path. 13 | // We can't use realpath here because the file does not exist yet. 14 | 15 | if (!path || path[0] == '\0') { 16 | luaL_error(L, "Invalid file path"); 17 | return NULL; 18 | } 19 | 20 | if (path[0] == '/') { 21 | // Absolute path, return as is 22 | return strdup(path); 23 | } else { 24 | // Relative path, prepend current working directory 25 | char cwd[PATH_MAX]; 26 | if (getcwd(cwd, sizeof(cwd)) == NULL) { 27 | luaL_error(L, "Failed to get current working directory: %s", strerror(errno)); 28 | return NULL; 29 | } 30 | 31 | size_t full_path_len = strlen(cwd) + 1 + strlen(path) + 1; // cwd + '/' + path + '\0' 32 | char *full_path = malloc(full_path_len); 33 | if (!full_path) { 34 | luaL_error(L, "Memory allocation failed for full path"); 35 | return NULL; 36 | } 37 | 38 | snprintf(full_path, full_path_len, "%s/%s", cwd, path); 39 | return full_path; 40 | } 41 | } 42 | 43 | int rb_core_write_file(lua_State *L) { 44 | const char *filepath = luaL_checkstring(L, 1); 45 | size_t len; 46 | const char *data = luaL_checklstring(L, 2, &len); 47 | 48 | // Create parent directories if necessary (optional: not included here) 49 | // TODO: Move the fs.c from cli to librootbeer so it can be shared 50 | 51 | rb_ctx_t *ctx = rb_ctx_from_lua(L); 52 | filepath = rb_resolve_full_path(L, filepath); 53 | if (!filepath) { 54 | return luaL_error(L, "Failed to resolve full path for '%s'", lua_tostring(L, 1)); 55 | } 56 | 57 | int status = rb_track_gen_file(ctx, filepath); 58 | if (status != RB_OK) { 59 | // TODO: rb_strerror 60 | return luaL_error(L, "Failed to track file '%s': %d", filepath, status); 61 | } 62 | 63 | int fd = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0644); 64 | if (fd == -1) { 65 | return luaL_error(L, "Failed to open file '%s': %s", filepath, strerror(errno)); 66 | } 67 | 68 | ssize_t written = write(fd, data, len); 69 | close(fd); 70 | 71 | if (written < 0 || (size_t)written != len) { 72 | unlink(filepath); 73 | return luaL_error(L, "Failed to write to file '%s': %s", filepath, strerror(errno)); 74 | } 75 | 76 | 77 | lua_pushstring(L, filepath); 78 | return 1; 79 | } 80 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/lua_config/rb_ctx_state.c: -------------------------------------------------------------------------------- 1 | #include "rb_ctx_state.h" 2 | #include "lua.h" 3 | #include "rb_ctx.h" 4 | #include "rb_strlist.h" 5 | #include 6 | #include 7 | 8 | rb_ctx_t *rb_ctx_from_lua(lua_State *L) { 9 | lua_pushlightuserdata(L, (void *)rb_ctx_from_lua); 10 | lua_gettable(L, LUA_REGISTRYINDEX); 11 | rb_ctx_t *ctx = (rb_ctx_t *)lua_touserdata(L, -1); 12 | 13 | assert(ctx != NULL); 14 | return ctx; 15 | } 16 | 17 | rb_ctx_t *rb_ctx_init(void) { 18 | rb_ctx_t *ctx = malloc(sizeof(rb_ctx_t)); 19 | if (ctx == NULL) { 20 | return NULL; 21 | } 22 | 23 | // We skip initializing the script_path and script_dir since 24 | // those will only be available once the CLI is invoked. 25 | ctx->script_path = NULL; 26 | ctx->script_dir = NULL; 27 | 28 | // TODO: Cleanup whatever is going on here & handle allocation failure 29 | rb_strlist_init(&ctx->lua_modules, RB_INIT_LUAMODULES_CAP); 30 | rb_strlist_init(&ctx->static_inputs, RB_INIT_STATICINPUTS_CAP); 31 | rb_idlist_init(&ctx->intermediates, RB_INIT_INTERMEDIATES_CAP); 32 | rb_strlist_init(&ctx->generated, RB_INIT_GENERATED_CAP); 33 | 34 | ctx->lua_files = malloc(RB_CTX_LUAFILES_MAX * sizeof(char *)); 35 | if (ctx->lua_files == NULL) { 36 | free(ctx); 37 | return NULL; 38 | } 39 | 40 | ctx->lua_files_count = 0; 41 | for (size_t i = 0; i < RB_CTX_LUAFILES_MAX; i++) { 42 | ctx->lua_files[i] = NULL; 43 | } 44 | 45 | ctx->ext_files = malloc(RB_CTX_EXTFILES_MAX * sizeof(char *)); 46 | if (ctx->ext_files == NULL) { 47 | free(ctx->lua_files); 48 | free(ctx); 49 | return NULL; 50 | } 51 | 52 | ctx->ext_files_count = 0; 53 | for (size_t i = 0; i < RB_CTX_EXTFILES_MAX; i++) { 54 | ctx->ext_files[i] = NULL; 55 | } 56 | 57 | ctx->plugin_transforms = malloc(RB_CTX_TRANSFORMS_MAX * sizeof(char *)); 58 | if (ctx->plugin_transforms == NULL) { 59 | free(ctx->ext_files); 60 | free(ctx->lua_files); 61 | free(ctx); 62 | return NULL; 63 | } 64 | 65 | ctx->plugin_transforms_count = 0; 66 | for (size_t i = 0; i < RB_CTX_TRANSFORMS_MAX; i++) { 67 | ctx->plugin_transforms[i] = NULL; 68 | } 69 | 70 | return ctx; 71 | } 72 | 73 | void rb_ctx_free(rb_ctx_t *rb_ctx) { 74 | if (rb_ctx == NULL) { 75 | return; 76 | } 77 | 78 | for (size_t i = 0; i < rb_ctx->lua_files_count; i++) { 79 | free(rb_ctx->lua_files[i]); 80 | } 81 | 82 | 83 | for (size_t i = 0; i < rb_ctx->ext_files_count; i++) { 84 | free(rb_ctx->ext_files[i]); 85 | } 86 | 87 | for (size_t i = 0; i < rb_ctx->plugin_transforms_count; i++) { 88 | free(rb_ctx->plugin_transforms[i]); 89 | } 90 | 91 | free(rb_ctx->lua_files); 92 | free(rb_ctx->ext_files); 93 | free(rb_ctx->plugin_transforms); 94 | 95 | free(rb_ctx); 96 | return; 97 | } 98 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/cli_cmds/apply.c: -------------------------------------------------------------------------------- 1 | #include "cli_module.h" 2 | #include "lua_init.h" 3 | #include "rb_ctx.h" 4 | #include "rb_ctx_state.h" 5 | #include 6 | #include 7 | 8 | // The apply command is where we tell rootbeer to interpret the lua 9 | // configuration and create a new system configuration revision. 10 | 11 | void rb_cli_apply_print_usage() { 12 | printf("Usage: rootbeer apply \n"); 13 | } 14 | 15 | int rb_cli_apply_func(const int argc, const char *argv[]) { 16 | // Check for sudo permissions 17 | if (geteuid() != 0) { 18 | fprintf(stderr, "error: rootbeer apply must be run as root\n"); 19 | return 1; 20 | } 21 | 22 | // Check argument for config file 23 | if (argc < 3) { 24 | fprintf(stderr, "Usage: %s %s \n", argv[0], argv[1]); 25 | return 1; 26 | } 27 | 28 | // Check access permissions on the config file 29 | if (access(argv[2], F_OK | R_OK) != 0) { 30 | fprintf(stderr, "error: could not access config file\n"); 31 | return 1; 32 | } 33 | 34 | // Drop privileges until we after lua 35 | if (seteuid(getuid()) != 0) { 36 | fprintf(stderr, "error: could not drop privileges\n"); 37 | return 1; 38 | } 39 | 40 | rb_ctx_t *rb_ctx = rb_ctx_init(); 41 | rb_ctx->lua_state = luaL_newstate(); 42 | rb_ctx->script_path = strdup((const char *)argv[2]); 43 | rb_ctx->script_dir = dirname(strdup((const char *)argv[2])); 44 | int i = lua_runtime_init(rb_ctx->lua_state, rb_ctx->script_path); 45 | if (i != 0) { 46 | fprintf(stderr, "error: could not initialize lua runtime\n"); 47 | rb_ctx_free(rb_ctx); 48 | return 1; 49 | } 50 | 51 | int j = lua_register_context(rb_ctx->lua_state, rb_ctx); 52 | if (j != 0) { 53 | fprintf(stderr, "error: could not register context in lua\n"); 54 | rb_ctx_free(rb_ctx); 55 | return 1; 56 | } 57 | 58 | int status = luaL_dofile(rb_ctx->lua_state, rb_ctx->script_path); 59 | if (status != LUA_OK) { 60 | fprintf(stderr, "Failed to execute lua configuration:\n"); 61 | fprintf(stderr, "%s\n", lua_tostring(rb_ctx->lua_state, -1)); 62 | lua_pop(rb_ctx->lua_state, 1); 63 | return 1; 64 | } 65 | 66 | // Restore privileges 67 | if (seteuid(0) != 0) { 68 | fprintf(stderr, "error: could not restore privileges\n"); 69 | return 1; 70 | } 71 | 72 | // Print all the lua_files 73 | for (size_t i = 0; i < rb_ctx->lua_files_count; i++) { 74 | printf("Lua file: %s\n", rb_ctx->lua_files[i]); 75 | } 76 | 77 | lua_close(rb_ctx->lua_state); 78 | rb_ctx_free(rb_ctx); 79 | 80 | printf("Revision created successfully\n"); 81 | return 0; 82 | } 83 | 84 | rb_cli_cmd apply = { 85 | "apply", 86 | "Apply a lua configuration and generate a new revision for your system", 87 | rb_cli_apply_print_usage, 88 | rb_cli_apply_func 89 | }; 90 | -------------------------------------------------------------------------------- /src/librootbeer/src/helpers/idlist.c: -------------------------------------------------------------------------------- 1 | #include "rb_idlist.h" 2 | #include 3 | #include 4 | 5 | int rb_idlist_init(rb_idlist_t *list, size_t initial_capacity) { 6 | list->count = 0; 7 | list->capacity = initial_capacity; 8 | list->ids = malloc(initial_capacity * sizeof(char *)); 9 | list->paths = malloc(initial_capacity * sizeof(char *)); 10 | if (!list->ids || !list->paths) { 11 | return -1; 12 | } 13 | 14 | return 0; 15 | } 16 | 17 | /** 18 | * Resize the ID list to a new capacity. 19 | * 20 | * @param list Pointer to the ID list. 21 | * @return 0 on success, -1 on failure. 22 | */ 23 | static int rb_idlist_resize(rb_idlist_t *list) { 24 | // 4 is a safe default capacity for small lists. 25 | size_t new_capacity = list->capacity == 0 ? 4 : list->capacity * 2; 26 | char **new_ids = realloc(list->ids, new_capacity * sizeof(char *)); 27 | char **new_paths = realloc(list->paths, new_capacity * sizeof(char *)); 28 | if (!new_ids || !new_paths) { 29 | free(new_ids); 30 | free(new_paths); 31 | return -1; 32 | } 33 | 34 | list->ids = new_ids; 35 | list->paths = new_paths; 36 | list->capacity = new_capacity; 37 | return 0; 38 | } 39 | 40 | int rb_idlist_add(rb_idlist_t *list, const char *id, const char *path) { 41 | for (size_t i = 0; i < list->count; i++) { 42 | // Update the path if the ID already exists. 43 | if (strcmp(list->ids[i], id) == 0) { 44 | free(list->paths[i]); 45 | list->paths[i] = strdup(path); 46 | if (!list->paths[i]) { 47 | return -1; 48 | } 49 | 50 | return 0; 51 | } 52 | } 53 | 54 | if (list->count >= list->capacity) { 55 | if (rb_idlist_resize(list) < 0) { 56 | return -1; 57 | } 58 | } 59 | 60 | list->ids[list->count] = strdup(id); 61 | if (!list->ids[list->count]) { 62 | return -1; 63 | } 64 | 65 | list->paths[list->count] = strdup(path); 66 | if (!list->paths[list->count]) { 67 | free(list->ids[list->count]); 68 | return -1; 69 | } 70 | 71 | list->count++; 72 | return 0; 73 | } 74 | 75 | const char *rb_idlist_get(const rb_idlist_t *list, const char *id) { 76 | if (list == NULL || list->ids == NULL) { 77 | return NULL; 78 | } 79 | 80 | for (size_t i = 0; i < list->count; i++) { 81 | if (strcmp(list->ids[i], id) == 0) { 82 | return list->paths[i]; 83 | } 84 | } 85 | 86 | return NULL; 87 | } 88 | 89 | void rb_idlist_free(rb_idlist_t *list) { 90 | if (list == NULL) { 91 | return; 92 | } 93 | 94 | for (size_t i = 0; i < list->count; i++) { 95 | free(list->ids[i]); 96 | free(list->paths[i]); 97 | } 98 | 99 | free(list->ids); 100 | free(list->paths); 101 | 102 | list->ids = NULL; 103 | list->paths = NULL; 104 | list->count = 0; 105 | list->capacity = 0; 106 | } 107 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install Mise 17 | run: | 18 | curl https://mise.run | sh 19 | echo "$HOME/.local/share/mise/bin" >> $GITHUB_PATH 20 | echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH 21 | 22 | - name: Cache Tools 23 | uses: actions/cache@v4 24 | with: 25 | path: ~/.local/share/mise 26 | key: mise-${{ runner.os }}-${{ hashFiles('mise.toml') }} 27 | 28 | - name: Install Tools 29 | run: | 30 | mise install --yes 31 | 32 | - name: Cache Dependencies 33 | uses: actions/cache@v4 34 | with: 35 | path: | 36 | subprojects/** 37 | !subprojects/*.wrap 38 | key: deps-${{ runner.os }}-${{ hashFiles('subprojects/*.wrap') }} 39 | 40 | - name: Build 41 | run: mise run build 42 | build-docs: 43 | runs-on: ubuntu-latest 44 | permissions: 45 | contents: read 46 | id-token: write 47 | pages: write 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.upload-docs.outputs.page_url }} 51 | 52 | steps: 53 | - name: Checkout Repository 54 | uses: actions/checkout@v4 55 | 56 | - name: Install Mise 57 | run: | 58 | curl https://mise.run | sh 59 | echo "$HOME/.local/share/mise/bin" >> $GITHUB_PATH 60 | echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH 61 | 62 | - name: Cache Tools 63 | uses: actions/cache@v4 64 | with: 65 | path: ~/.local/share/mise 66 | key: mise-${{ runner.os }}-${{ hashFiles('mise.toml') }} 67 | 68 | - name: Install Tools 69 | run: | 70 | mise install --yes 71 | 72 | - name: Run doxygen 73 | uses: mattnotmitt/doxygen-action@v1 74 | with: 75 | working-directory: docs/ 76 | doxyfile-path: Doxyfile 77 | 78 | - name: Cache Python Dependencies 79 | uses: actions/cache@v4 80 | with: 81 | path: .venv 82 | key: python-deps-${{ runner.os }}-${{ hashFiles('docs/requirements.txt') }} 83 | restore-keys: | 84 | python-deps-${{ runner.os }}- 85 | 86 | - name: Run Sphinx 87 | run: mise run docs-ci 88 | 89 | - name: Upload Documentation 90 | id: upload-docs 91 | uses: actions/upload-pages-artifact@v3 92 | with: 93 | path: docs/_build/html 94 | 95 | - name: Deploy Documentation 96 | uses: actions/deploy-pages@v4 97 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/util/fs.c: -------------------------------------------------------------------------------- 1 | #include "rootbeer.h" 2 | 3 | // Creates a directory but recursively creates parent directories 4 | // Starts from an empty path and uses slashes instead of dirname(3) 5 | // Recursively done by checking all parents and working from EEXIST upwards 6 | int rb_create_dir(char *path) { 7 | char tmp[PATH_MAX]; 8 | snprintf(tmp, sizeof(tmp), "%s", path); 9 | size_t len = strlen(tmp); 10 | 11 | // Drop trailing slash 12 | if (tmp[len - 1] == '/') { 13 | tmp[len - 1] = '\0'; 14 | } 15 | 16 | char *end = NULL; 17 | for (end = tmp + 1; *end; end++) { 18 | if (*end == '/') { 19 | *end = '\0'; // Terminate so we have a path component 20 | 21 | if (mkdir(tmp, 0775) != 0 && errno != EEXIST) { 22 | fprintf(stderr, "error: could not create directory\n"); 23 | fprintf(stderr, "error: %s\n", strerror(errno)); 24 | return 1; 25 | } 26 | 27 | *end = '/'; // Restore the slash 28 | } 29 | } 30 | 31 | if (mkdir(path, 0775) != 0 && errno != EEXIST) { 32 | fprintf(stderr, "error: could not create directory\n"); 33 | fprintf(stderr, "error: %s\n", strerror(errno)); 34 | return 1; 35 | } 36 | 37 | return 0; 38 | } 39 | 40 | int rb_copy_file(const char *src, const char *dst) { 41 | FILE *src_file = fopen(src, "r"); 42 | if (src_file == NULL) { 43 | printf("error: could not open source file\n"); 44 | return 1; 45 | } 46 | 47 | if (rb_create_dir(dirname((char *)dst)) != 0) { 48 | fclose(src_file); 49 | return 1; 50 | } 51 | 52 | FILE *dst_file = fopen(dst, "w"); 53 | printf("copying %s to %s\n", src, dst); 54 | if (dst_file == NULL) { 55 | printf("error: could not open destination file\n"); 56 | fclose(src_file); 57 | return 1; 58 | } 59 | 60 | char buffer[4096]; 61 | size_t bytes; 62 | while ((bytes = fread(buffer, 1, sizeof(buffer), src_file)) > 0) { 63 | fwrite(buffer, 1, bytes, dst_file); 64 | } 65 | 66 | fclose(src_file); 67 | fclose(dst_file); 68 | return 0; 69 | } 70 | 71 | char **rb_recurse_files(const char *path, int *count) { 72 | DIR *dir = opendir(path); 73 | if (dir == NULL) { 74 | printf("error: could not open directory\n"); 75 | return NULL; 76 | } 77 | 78 | struct dirent *entry; 79 | int c = 0; 80 | while ((entry = readdir(dir)) != NULL) { 81 | if (entry->d_type == DT_REG) { 82 | c++; 83 | } 84 | } 85 | 86 | *count = c; 87 | if (c == 0) { 88 | return NULL; 89 | } 90 | 91 | char **files = calloc(c, sizeof(char *)); 92 | if (files == NULL) { 93 | printf("error: could not allocate memory\n"); 94 | return NULL; 95 | } 96 | 97 | rewinddir(dir); 98 | int idx = 0; 99 | while ((entry = readdir(dir)) != NULL) { 100 | if (entry->d_type == DT_REG) { 101 | files[idx] = malloc(strlen(entry->d_name) + 1); 102 | strcpy(files[idx], entry->d_name); 103 | idx++; 104 | } 105 | } 106 | 107 | closedir(dir); 108 | return files; 109 | } 110 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/rev_store/read.c: -------------------------------------------------------------------------------- 1 | #include "store_module.h" 2 | 3 | // Helper function to parse a comma-separated list of strings 4 | // The revision _meta stores all ref and cfg files in this format 5 | void rb_read_list(char ***list, int *count, char *line) { 6 | int c = 0; 7 | for (int i = 0; i < strlen(line); i++) { 8 | if (line[i] == ',') { 9 | c++; 10 | } 11 | } 12 | 13 | // If there are no commas, then there is only one element 14 | c++; 15 | *count = c; 16 | 17 | // Allocate the array 18 | assert(*list == NULL); 19 | assert(*count >= 0); 20 | 21 | *list = calloc(c, sizeof(char *)); 22 | char *token = strtok(line, ","); 23 | int idx = 0; 24 | 25 | while (token != NULL && idx < c) { 26 | (*list)[idx] = malloc(strlen(token) + 1); 27 | strncpy((*list)[idx], token, strlen(token)); 28 | token = strtok(NULL, ","); 29 | idx++; 30 | } 31 | } 32 | 33 | // Reads a revision from the store by its ID. 34 | rb_revision_t *rb_store_get_revision_by_id(const int id) { 35 | char *revision_path = malloc(strlen(STORE_ROOT) + strlen("/store/") + 10 + 1); 36 | sprintf(revision_path, "%s/store/%d", STORE_ROOT, id); 37 | 38 | if (access(revision_path, F_OK | R_OK) != 0) { 39 | printf("error: revision does not exist?\n"); 40 | free(revision_path); 41 | return NULL; 42 | } 43 | 44 | FILE *file = fopen(revision_path, "r"); 45 | if (file == NULL) { 46 | printf("error: could not open revision file\n"); 47 | free(revision_path); 48 | return NULL; 49 | } 50 | 51 | // Read the _meta file in the revision directory 52 | char *meta_path = malloc(strlen(revision_path) + strlen("/_meta") + 1); 53 | sprintf(meta_path, "%s/_meta", revision_path); 54 | 55 | FILE *meta_file = fopen(meta_path, "r"); 56 | if (meta_file == NULL) { 57 | printf("error: could not open meta file\n"); 58 | free(revision_path); 59 | free(meta_path); 60 | fclose(file); 61 | return NULL; 62 | } 63 | 64 | rb_revision_t *rev = malloc(sizeof(rb_revision_t)); 65 | rev->id = id; 66 | 67 | // Read the meta file in this format 68 | // name: 69 | // timestamp: 70 | // cfg_files: (comma separated) 71 | // ref_files: (comma separated) 72 | char buffer[1024]; 73 | 74 | // Read the name 75 | while (fgets(buffer, 1024, meta_file) != NULL) { 76 | if (strncmp(buffer, "name: ", 6) == 0) { 77 | rev->name = malloc(strlen(buffer) - 6); 78 | sscanf(buffer, "name: %s", rev->name); 79 | continue; 80 | } 81 | 82 | if (strncmp(buffer, "timestamp: ", 11) == 0) { 83 | sscanf(buffer, "timestamp: %ld", &rev->timestamp); 84 | continue; 85 | } 86 | 87 | if (strncmp(buffer, "cfg_files: ", 11) == 0) { 88 | rb_read_list(&rev->cfg_filesv, &rev->cfg_filesc, buffer + 11); 89 | continue; 90 | } 91 | 92 | if (strncmp(buffer, "ref_files: ", 11) == 0) { 93 | rb_read_list(&rev->ref_filesv, &rev->ref_filesc, buffer + 11); 94 | continue; 95 | } 96 | } 97 | 98 | free(revision_path); 99 | free(meta_path); 100 | fclose(file); 101 | fclose(meta_file); 102 | return rev; 103 | } 104 | -------------------------------------------------------------------------------- /docs/source/c_api/rb_plugin.md: -------------------------------------------------------------------------------- 1 | # Creating a Rootbeer Plugin 2 | :::{tip} 3 | This document covers creating a Rootbeer plugin in C. For information on 4 | riting plugins in Lua, see the [Lua API documentation]({doc}`lua_api`). 5 | ::: 6 | 7 | Plugins are what allow a user to define system configurations and behaviors. 8 | Without plugins, Rootbeer is essentially a glorified Lua interpreter. Your 9 | plugin is able to use the public Rootbeer API to hook into the revision 10 | system and define extra files that can be used for a system configuration. 11 | 12 | ## Getting Started 13 | At its core, a Rootbeer plugin is a static C library that is compiled into 14 | the main `rb` executable. The plugin defines various functions that are 15 | exposed into the Lua environment, allowing the user to interact with your 16 | plugin. 17 | 18 | To get started, you'll want to create a new directory in `src/plugins` 19 | and create a `meson.build` file in that directory. This file will 20 | look something like this: 21 | 22 | ```meson 23 | rootbeer_lib = static_library( 24 | 'myplugin_name', 25 | sources: files( 26 | 'src/main.c', 27 | ... 28 | ), 29 | # Root include allows you to access the Rootbeer API headers 30 | include_directories: [include_directories('src'), root_include], 31 | 32 | # The only required dependency is luajit, which is used to 33 | # interact with the Lua environment. You can add more dependencies 34 | # as needed for the functionality of your plugin. 35 | dependencies: [dependency('luajit')], 36 | ) 37 | 38 | # These 2 are VERY important, the name tells exactly how the plugin 39 | # will be registered in the Lua environment (`rootbeer.myplugin_name`). 40 | register_plugin_lib = rootbeer_lib 41 | register_plugin_name = 'myplugin_name' 42 | ``` 43 | 44 | :::{warning} 45 | In theory you can use another build system, but the Rootbeer build 46 | system is designed to work with Meson. Try at your own risk. 47 | ::: 48 | 49 | ## Plugin Structure 50 | Let's take a look at what is inside that `src/main.c` file. This is the 51 | entry point for your plugin and is where you will define the functions 52 | that will be exposed to the Lua environment. A simple plugin may look like this: 53 | 54 | ```c 55 | #include "rb_plugin.h" 56 | 57 | const luaL_Reg myplugin_funcs[] = { 58 | {"my_function", my_function}, 59 | {NULL, NULL} // REQUIRED to terminate the array 60 | }; 61 | 62 | // It's imperative that myplugin_name matches the name in the meson.build 63 | // file in the `register_plugin_name` variable. 64 | RB_PLUGIN(myplugin_name, "Description", "0.0.1", myplugin_funcs) 65 | ``` 66 | 67 | You can define your own functions in the `my_function` variable, which 68 | will be called when the user calls `rootbeer.myplugin_name.my_function()` 69 | from Lua. These plugins are expected to be global functions in your plugin. 70 | 71 | From here you can get as complex as you want and split up your plugin 72 | into multiple files. As long as you include the `rb_plugin.h` header 73 | and the call to `RB_PLUGIN`, you can define as many functions as you want. 74 | 75 | ## `rb_plugin.h` Reference 76 | ```{doxygenfile} include/rb_plugin.h 77 | :project: Rootbeer 78 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/cli_cmds/store.c: -------------------------------------------------------------------------------- 1 | #include "cli_module.h" 2 | #include "store_module.h" 3 | 4 | // The store command is used to manage the revision store for the system. 5 | // This includes initializing the store, creating new revisions, switching 6 | // to a specific revision, and listing all revisions. 7 | 8 | // TODO: Recursive subcommands struct 9 | void rb_cli_store_print_usage() { 10 | printf("Usage: rootbeer store \n"); 11 | printf("Commands:\n"); 12 | printf(" init: Initialize the revision store\n"); 13 | printf(" destroy: Destroy the revision store\n"); 14 | printf(" list: List all revisions\n"); 15 | printf(" read: Read a specific revision\n"); 16 | } 17 | 18 | int rb_cli_store_init() { 19 | rb_store_init_or_die(); 20 | return 0; 21 | } 22 | 23 | int rb_cli_store_destroy() { 24 | rb_store_destroy(); 25 | return 0; 26 | } 27 | 28 | int rb_cli_store_read(int id) { 29 | rb_revision_t *rev; 30 | if (id < 0) { 31 | rev = rb_store_get_current_revision(); 32 | } else { 33 | rev = rb_store_get_revision_by_id(id); 34 | } 35 | 36 | if (rev == NULL) { 37 | printf("Revision not found.\n"); 38 | return 1; 39 | } 40 | 41 | printf("Revision %d\n", rev->id); 42 | printf("Name: %s\n", rev->name); 43 | printf("Timestamp: %s", asctime(localtime(&rev->timestamp))); 44 | printf("Config files count: %d\n", rev->cfg_filesc); 45 | printf("Reference files count: %d\n", rev->ref_filesc); 46 | 47 | for (int i = 0; i < rev->cfg_filesc; i++) { 48 | if (rev->cfg_filesv == NULL) { 49 | printf("Config file %d: NULL\n", i); 50 | continue; 51 | } 52 | printf("Config file %d: %s\n", i, rev->cfg_filesv[i]); 53 | } 54 | 55 | for (int i = 0; i < rev->ref_filesc; i++) { 56 | printf("Reference file %d: %s\n", i, rev->ref_filesv[i]); 57 | } 58 | return 0; 59 | } 60 | 61 | int rb_cli_store_list() { 62 | int count = rb_store_get_revision_count(); 63 | if (count == 0) { 64 | printf("No revisions found.\n"); 65 | return 0; 66 | } 67 | 68 | rb_revision_t **revs = rb_store_get_all(count); 69 | for (int i = 0; i < count; i++) { 70 | rb_revision_t *rev = revs[i]; 71 | char time_buf[64]; 72 | 73 | strftime( 74 | time_buf, sizeof(time_buf), 75 | "%Y-%m-%d %H:%M:%S", localtime(&rev->timestamp) 76 | ); 77 | 78 | printf("[%d] %s (%s)\n", rev->id, rev->name, time_buf); 79 | } 80 | 81 | return 0; 82 | } 83 | 84 | int rb_cli_store_func(const int argc, const char *argv[]) { 85 | if (strcmp(argv[2], "init") == 0) { 86 | return rb_cli_store_init(); 87 | } 88 | 89 | if (strcmp(argv[2], "destroy") == 0) { 90 | return rb_cli_store_destroy(); 91 | } 92 | 93 | if (strcmp(argv[2], "list") == 0) { 94 | return rb_cli_store_list(); 95 | } 96 | 97 | if (strcmp(argv[2], "read") == 0) { 98 | if (argc < 4) { 99 | // Return the current revision if no id is provided. 100 | return rb_cli_store_read(-1); 101 | } 102 | 103 | int id; 104 | int res = sscanf(argv[3], "%d", &id); 105 | if (res != 1) { 106 | printf("Invalid revision id.\n"); 107 | return 1; 108 | } 109 | 110 | return rb_cli_store_read(id); 111 | } 112 | 113 | return 0; 114 | } 115 | 116 | rb_cli_cmd store = { 117 | "store", 118 | "Command to view, manipulate, and go through your revision store", 119 | rb_cli_store_print_usage, 120 | rb_cli_store_func 121 | }; 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rootbeer 2 | > Deterministically manage your system configuration with lua! 3 | 4 | This tool aims to make managing your system configuration easy. 5 | You can think of it as a tool that's very similar in function to 6 | [home-manager](https://github.com/nix-community/home-manager). 7 | The idea is that you can define exactly how you want your system (or userland) 8 | to be configured within a lua script and then run the tool to apply those 9 | changes to your system. 10 | 11 | Why not Nix? I have plenty of reasons why I don't like Nix which I go into on 12 | my [blog post](https://tale.me/blog/nix-might-be-overengineered/). However, 13 | I'm also not a fan of the `nix` language and wanted to see if I could make 14 | something similar with a language that is fully functional, scriptable, and 15 | has good error messages (lua). Also, this is a fun project to work on! 16 | 17 | ## Goals 18 | > At the moment this project will not work if you try to compile it. 19 | > Much of the functionality is just flat out missing and is TODO. 20 | 21 | - Be able to define system configurations in a lua script 22 | - Maintain a store of configurations, allowing rollbacks and revisions 23 | - Interface with OS-specific tools to apply configurations 24 | - Package managers such as `brew` on macOS or `dnf` on Fedora 25 | - Daemon services such as `launchd` on macOS or `systemd` on Linux 26 | 27 | ## Technical Components 28 | I plan on extracting this out to separate documentation later, but here's a 29 | quick overview of the technical components of this project and how they all 30 | work together. The end result is `rootbeer`, a CLI tool to control everything. 31 | 32 | Here are the components: 33 | - An embedded LuaJIT interpreter to load and evaluate lua scripts. All of the 34 | functionality provided by Rootbeer will be exposed to the lua script via the 35 | `rootbeer` module. 36 | 37 | - A store system to manage configurations. This will be similar to Nix's store 38 | system, where configurations are stored in a directory and can be rolled back 39 | or revised. 40 | 41 | - A pluggable module system that serves as the basis of all functionality 42 | offered by Rootbeer. The idea is that different integrations with the OSes 43 | can be written as modules and compiled in-tree to ship in the final binary. 44 | 45 | ## Building 46 | The project is still in its early stages, stuff might break! 47 | The only prerequisite is that you have `meson` and `ninja` installed. 48 | 49 | You'll need to setup and run the build like so: 50 | ```bash 51 | meson setup build 52 | meson compile -C build 53 | ``` 54 | 55 | This will create the `./build/rb` binary which you can run to 56 | interact with the tool. 57 | 58 | I highly recommend using [mise](https://mise.jdx.dev/) for anything else 59 | as I have automatically implemented almost all the commands you need and 60 | dependencies. With mise you can just run `mise run build`. 61 | 62 | ### Building documentation 63 | The documentation is hard to build mostly because Doxygen is not available 64 | in a portable way. You'll need to preinstall Doxygen and ensure it is 65 | in your `PATH`. You'll also need to activate a python virtualenv (automatically 66 | handled if you are using mise) and install the `docs/requirements.txt`. 67 | 68 | Once done, you can build the documentation with: 69 | ```bash 70 | cd docs/ 71 | doxygen 72 | sphinx-build -b html source _build/html 73 | 74 | # Or with mise: mise build docs 75 | ``` 76 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A Nix flake for the tale/rootbeer system configuration tool"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | 7 | flake-parts = { 8 | url = "github:hercules-ci/flake-parts"; 9 | inputs.nixpkgs-lib.follows = "nixpkgs"; 10 | }; 11 | 12 | systems = { 13 | url = "github:nix-systems/default"; 14 | }; 15 | }; 16 | 17 | outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { 18 | systems = import inputs.systems; 19 | perSystem = { self', lib, pkgs, system, ... }: { 20 | packages = { 21 | rootbeer = let 22 | # NOTE: as per <./meson.build> 23 | version = "0.0.1"; 24 | source-with-meson-deps = pkgs.stdenv.mkDerivation { 25 | pname = "rootbeer-deps"; 26 | inherit version; 27 | 28 | src = lib.cleanSource ./.; 29 | 30 | nativeBuildInputs = [ 31 | pkgs.pkg-config 32 | pkgs.meson 33 | pkgs.cmake 34 | pkgs.git 35 | pkgs.cacert 36 | pkgs.ninja 37 | ]; 38 | 39 | outputHashAlgo = "sha256"; 40 | outputHashMode = "recursive"; 41 | outputHash = "sha256-DdbXF0LdCptpY5zDApOUDHonDlMjRl7ut9m/46Lj+74="; 42 | 43 | phases = [ 44 | "installPhase" 45 | ]; 46 | 47 | installPhase = '' 48 | # Copy over the raw source in the output, since we'll do the vendoring work there 49 | mkdir -p $out 50 | cd $out 51 | cp --no-preserve=mode -r $src/. . 52 | 53 | # Set the SSL certificate file for network access 54 | export NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt 55 | 56 | # Download the subprojects the `meson` way (using networking, hence the FOD) 57 | ${pkgs.lib.getExe pkgs.meson} subprojects download \ 58 | cjson luajit 59 | 60 | # Clean up `git` metadata from the downloaded dependencies 61 | find subprojects -type d -name .git -prune -execdir rm -r {} + 62 | ''; 63 | }; 64 | in pkgs.stdenv.mkDerivation { 65 | pname = "rootbeer"; 66 | inherit version; 67 | 68 | src = source-with-meson-deps; 69 | 70 | nativeBuildInputs = [ 71 | pkgs.pkg-config 72 | pkgs.ninja 73 | pkgs.meson 74 | pkgs.autoPatchelfHook 75 | ]; 76 | 77 | buildInputs = [ 78 | ]; 79 | 80 | postInstall = '' 81 | # Manually install the main executable, left in the `src` directory, for some reason 82 | mkdir -p $out/bin 83 | cp ./src/rootbeer_cli/rb $out/bin/ 84 | 85 | # Install provided libraries 86 | # TODO: not sure what the structure of those should be for other consumers 87 | mkdir -p $out/lib/rootbeer 88 | cp -r ./src/librootbeer/. $out/lib/rootbeer/ 89 | ''; 90 | 91 | meta = with pkgs.lib; { 92 | description = "A tool to deterministically manage your system using Lua"; 93 | homepage = "Deterministically manage your system using Lua!"; 94 | # NOTE: as per <./meson.build> 95 | license = licenses.mit; 96 | platforms = platforms.all; 97 | maintainers = with maintainers; [ ]; 98 | }; 99 | }; 100 | 101 | default = self'.packages.rootbeer; 102 | }; 103 | 104 | devShells = { 105 | default = pkgs.mkShell { 106 | packages = self'.packages.rootbeer.buildInputs ++ self'.packages.rootbeer.nativeBuildInputs; 107 | }; 108 | }; 109 | }; 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /src/internal_include/rb_ctx.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rb_ctx.h 3 | * @brief Internal tracked context for the Rootbeer execution environment. 4 | * 5 | * When executing the lua script, Rootbeer needs to keep track of the context 6 | * in which the script is running to accurately generate a revision. The actual 7 | * logic for generating a revision is in @ref rb_revision.h. This context merely 8 | * keeps tracks of all the necessary inputs and desired outputs which can be 9 | * used to deterministically generate a revision. 10 | */ 11 | #ifndef RB_CTX_H 12 | #define RB_CTX_H 13 | 14 | #include 15 | #include "rb_idlist.h" 16 | #include "rb_rootbeer.h" 17 | #include "rb_strlist.h" 18 | 19 | /** 20 | * @brief Rootbeer context structure. 21 | * This is the main structure that keeps track of all the necessary 22 | * parameters to deterministically generate a revision. It gets populated 23 | * when `rb apply ` is called. 24 | */ 25 | struct rb_ctx_t { 26 | lua_State *lua_state; //!< Lua state from starting the LuaJIT VM. 27 | char *script_path; //!< Absolute path to the Lua script being executed. 28 | char *script_dir; //!< Directory containing script_path. 29 | 30 | //! Extra Lua modules that are required by the script. 31 | //! These are tracked through our `require` hook in the Lua VM. 32 | rb_strlist_t lua_modules; 33 | 34 | //! Additional static inputs that are used by the script. 35 | //! These can be templates, files to link, anything non-generated. 36 | rb_strlist_t static_inputs; 37 | 38 | //! Internal list of files generated by plugins to aid revision storage. 39 | //! These are stored inside the revision and store historical data. 40 | rb_idlist_t intermediates; 41 | 42 | //! Outputs of the entire revision generation which go out of the store. 43 | //! This list is tracked to easily revert the revision if needed. 44 | rb_strlist_t generated; 45 | 46 | //! Filenames of extra lua scripts loaded, relative to the script path. 47 | //! The lua VM is setup to allow requiring files in `lua/`. 48 | char **lua_files; 49 | 50 | //! The number of extra lua scripts loaded, relative to the script path. 51 | size_t lua_files_count; 52 | 53 | //! Any extra files that are used as inputs to the plugins. They are always 54 | //! relative to the script path. Think similar to Nix's `builtins.readFile`. 55 | char **ext_files; 56 | 57 | //! The number of extra files that are used as inputs to the plugins. 58 | size_t ext_files_count; 59 | 60 | //! Any intermediate files generated by plugins to populate inputs. The best 61 | //! example of this is how packaging is structured, where given an array of 62 | //! packages, the plugin resolves all versions and package hashes. 63 | char **plugin_transforms; 64 | 65 | //! The number of intermediate files generated by plugins. 66 | size_t plugin_transforms_count; 67 | }; 68 | 69 | /** 70 | * @def LUA_LIB 71 | * @brief The path to the Lua library used by Rootbeer. 72 | * 73 | * This is the directory where the Lua scripts are located. It is used to 74 | * set up the Lua environment so that it can require files from this directory. 75 | */ 76 | #define LUA_LIB "/opt/rootbeer/lua" 77 | 78 | /** 79 | * @def RB_CTX_LUAFILES_MAX 80 | * @brief Maximum number of Lua files that can be tracked in the context. 81 | */ 82 | #define RB_CTX_LUAFILES_MAX 100 83 | 84 | /** 85 | * @def RB_CTX_EXTFILES_MAX 86 | * @brief Maximum number of extra files that can be tracked in the context. 87 | */ 88 | #define RB_CTX_EXTFILES_MAX 1000 89 | 90 | /** 91 | * @def RB_CTX_TRANSFORMS_MAX 92 | * @brief Maximum number of plugin transforms that can be tracked in the context. 93 | */ 94 | #define RB_CTX_TRANSFORMS_MAX 1000 95 | 96 | /** 97 | * @def RB_MAX_INTERMEDIATE_ID_LENGTH 98 | * @brief Maximum length of an intermediate ID. 99 | */ 100 | #define RB_MAX_INTERMEDIATE_ID_LENGTH 64 101 | 102 | #endif // RB_CTX_H 103 | -------------------------------------------------------------------------------- /include/rb_rootbeer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rb_rootbeer.h 3 | * 4 | * This header defines all of the publicly accessible APIs for Rootbeer plugins. 5 | * These can be called within a native Rootbeer plugin to interact with the 6 | * core Rootbeer revision system. 7 | * See \ghdir{src/plugins/rootbeer_core} for an example of a plugin. 8 | */ 9 | #ifndef RB_ROOTBEER_H 10 | #define RB_ROOTBEER_H 11 | 12 | /** 13 | * @brief Rootbeer context structure. 14 | * This is an opaque pointer to the internal Rootbeer context. 15 | * See @ref rb_ctx_t for more details. 16 | */ 17 | #include "lua.h" 18 | #include 19 | 20 | #ifdef __cplusplus 21 | extern "C" { 22 | #endif 23 | 24 | typedef struct rb_ctx_t rb_ctx_t; 25 | 26 | /** 27 | * Registers the provided filepath as a reference file. 28 | * Reference files are kept track of as "imported" files in the revision system. 29 | * For example, `rb.link_file()` uses this to track the source files for links. 30 | * 31 | * @param ctx The Lua context to track the file in. 32 | * @param path The ABSOLUTE path to the file to track. 33 | * @return 0 on success, or a negative error code on failure. 34 | * TODO: Make this use absolute paths instead of relative paths. 35 | */ 36 | int rb_track_ref_file(rb_ctx_t *ctx, const char *path); 37 | 38 | /** 39 | * Registers the provided filepath as a generated file. 40 | * Generated files are those that are created by a plugin at runtime. 41 | * For example, `rb.link_file()` uses this to track destination files for links. 42 | * 43 | * @param ctx The Lua context to track the file in. 44 | * @param path The ABSOLUTE path to the file to track. 45 | * @return 0 on success, or a negative error code on failure. 46 | */ 47 | int rb_track_gen_file(rb_ctx_t *ctx, const char *path); 48 | 49 | /** 50 | * Opens a handle to an "intermediate file". Intermediate files are used 51 | * to store temporary data that is not meant to be kept long-term. 52 | * They can be looked up by their ID at a later time. 53 | * 54 | * @param ctx The Lua context to open the intermediate file in. 55 | * @param id The ID of the intermediate file to open. 56 | * @return A file handle to the intermediate file, or NULL on failure. 57 | */ 58 | FILE *rb_open_intermediate(rb_ctx_t *ctx, const char *id); 59 | 60 | /** 61 | * Retrieves the content of an intermediate file by its ID. 62 | * This function reads the content of the intermediate file 63 | * and returns it as a string. 64 | * 65 | * @param ctx The Lua context to retrieve the intermediate file from. 66 | * @param id The ID of the intermediate file to retrieve. 67 | * @return A pointer to the content of the intermediate file, 68 | */ 69 | const char *rb_get_intermediate(rb_ctx_t *ctx, const char *id); 70 | 71 | /** 72 | * @def RB_OK 73 | * @brief The return code for a successful operation. 74 | */ 75 | #define RB_OK 0 76 | 77 | /** 78 | * @def RB_ULIMIT_EXTFILES 79 | * @brief The return code when the maximum external files limit is reached. 80 | */ 81 | #define RB_ULIMIT_EXTFILES -1001 82 | 83 | /** 84 | * @def RB_ULIMIT_TRANSFORMS 85 | * @brief The return code when the maximum plugin transforms limit is reached. 86 | */ 87 | #define RB_ULIMIT_TRANSFORMS -1002 88 | 89 | /** 90 | * @def RB_ENOENT 91 | * @brief The return code when a file or directory does not exist. 92 | */ 93 | #define RB_ENOENT -2 94 | 95 | /** 96 | * @def RB_EEXIST 97 | * @brief The return code when a file or directory already exists. 98 | */ 99 | #define RB_EACCES -13 100 | 101 | /** 102 | * Fetches the current Rootbeer context from the Lua state. 103 | * We store the context in Lua via a light userdata pointer, whic allows us to 104 | * fetch it easily from the Lua state and use it flexibly across the project. 105 | * To identify the context, we use this fetch function as the ID for the data. 106 | * 107 | * @param L The Lua state from which to fetch the context. 108 | * @return A pointer to the Rootbeer context. 109 | * @note This function will panic if the context is not set in the Lua state. 110 | */ 111 | rb_ctx_t *rb_ctx_from_lua(lua_State *L); 112 | 113 | #ifdef __cplusplus 114 | } // extern "C" 115 | #endif 116 | 117 | #endif // RB_ROOTBEER_H 118 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/lua_config/lua_init.c: -------------------------------------------------------------------------------- 1 | #include "lua_init.h" 2 | #include "rb_ctx.h" 3 | #include "rb_strlist.h" 4 | #include "rootbeer.h" 5 | #include "rb_plugin.h" 6 | #include 7 | #include 8 | 9 | // This is linked in by Meson when compiling the plugins. 10 | extern const rb_plugin_t *rb_plugins[]; 11 | 12 | int lua_runtime_require_hook(lua_State *L) { 13 | rb_ctx_t *ctx = rb_ctx_from_lua(L); 14 | const char *modname = luaL_checkstring(L, 1); 15 | if (modname == NULL) { 16 | return luaL_error(L, "recieved an invalid module name"); 17 | } 18 | 19 | // MARK: It is VERY important that nothing else manipulates the Lua stack 20 | // here as the require function is expected to return a single value and 21 | // we need to pass on the orig via the lua_pcall. 22 | lua_getglobal(L, "require_orig"); 23 | lua_pushstring(L, modname); 24 | if (lua_pcall(L, 1, 1, 0) != LUA_OK) { 25 | return luaL_error(L, "error loading %s: %s", modname, lua_tostring(L, -1)); 26 | } 27 | 28 | // Lua modules are dot separate to indicate directories, meaning we need 29 | // to do the work to convert them back into slashes for the filesystem. 30 | char modpath[strlen(modname) + 1]; 31 | strncpy(modpath, modname, strlen(modname) + 1); 32 | for (size_t i = 0; i < strlen(modpath); i++) { 33 | if (modpath[i] == '.') { 34 | modpath[i] = '/'; 35 | } 36 | } 37 | 38 | // The full path on disk falls in /lua/.lua 39 | size_t modname_len = strlen(modpath) + strlen("/lua/") + strlen(".lua") + 1; 40 | char filepath[modname_len]; 41 | snprintf(filepath, modname_len, "/lua/%s.lua", modpath); 42 | 43 | rb_strlist_add(&ctx->lua_modules, modname); 44 | return 1; 45 | } 46 | 47 | int lua_runtime_init(lua_State *L, const char *entry_file) { 48 | assert(entry_file != NULL); 49 | assert(L != NULL); 50 | 51 | // Add our baked-in Lua libraries to the path. 52 | luaL_openlibs(L); 53 | lua_getglobal(L, "package"); 54 | lua_getfield(L, -1, "path"); 55 | const char *lpath = lua_tostring(L, -1); 56 | lua_pop(L, 1); 57 | 58 | // In non-debug builds, we use the extracted rootbeer Lua libraries. 59 | // In debug, we just use the lua/ directory in the git source-tree. 60 | // In this case, we make the assumption that the CWD is the source root. 61 | char system_path[PATH_MAX]; 62 | #ifndef DEBUG 63 | snprintf( 64 | system_path, sizeof(system_path), 65 | "%s/?.lua;%s/?/init.lua", LUA_LIB, LUA_LIB 66 | ); 67 | #else 68 | char *pwd = getcwd(NULL, 0); 69 | if (pwd == NULL) { 70 | rb_fatal("Could not get current working directory"); 71 | } 72 | 73 | printf("DEBUG: Using %s/lua/ for Lua libraries\n", pwd); 74 | snprintf( 75 | system_path, sizeof(system_path), 76 | "%s/lua/?.lua;%s/lua/?/init.lua", pwd, pwd 77 | ); 78 | 79 | free(pwd); 80 | #endif 81 | 82 | size_t new_lpath_len = strlen(lpath) 83 | + strlen(system_path) 84 | + strlen("/lua/?.lua") 85 | + strlen("/lua/?/init.lua") 86 | + 3; // 2 semicolons and null terminator 87 | 88 | char *new_lpath = malloc(new_lpath_len); 89 | if (new_lpath == NULL) { 90 | rb_fatal("Failed to allocate space for modified lua path"); 91 | } 92 | 93 | snprintf(new_lpath, new_lpath_len, "%s;%s/lua/?.lua;%s/lua/?/init.lua", 94 | lpath, LUA_LIB, LUA_LIB); 95 | lua_pushstring(L, new_lpath); 96 | lua_setfield(L, -2, "path"); 97 | lua_pop(L, 1); 98 | free(new_lpath); 99 | 100 | // Setup our hook to the require function allowing us to track 101 | // which files are required by the Lua scripts. Keep in mind that there are 102 | // other ways to load Lua files, such as `dofile` or `loadfile`, but we 103 | // only choose to track the `require` function for now. 104 | // 105 | // TODO: Reconsider this if we find that we need to track more. 106 | lua_getglobal(L, "require"); 107 | lua_setglobal(L, "require_orig"); 108 | lua_pushcclosure(L, lua_runtime_require_hook, 1); 109 | lua_setglobal(L, "require"); 110 | 111 | // Load our plugins into the Lua environment. 112 | for (const rb_plugin_t **p = rb_plugins; *p != NULL; p++) { 113 | const rb_plugin_t *plugin = *p; 114 | char plugin_name[64]; 115 | 116 | // Handle the special case the plugin is called "__rootbeer__" 117 | // which is the rootbeer plugin itself and needs to be on the root. 118 | if (strcmp(plugin->plugin_name, "__rootbeer__") == 0) { 119 | snprintf(plugin_name, sizeof(plugin_name), "rootbeer"); 120 | } else { 121 | snprintf(plugin_name, sizeof(plugin_name), "rootbeer.%s", plugin->plugin_name); 122 | } 123 | 124 | printf("Loading plugin: %s\n", plugin_name); 125 | luaL_register(L, plugin_name, plugin->functions); 126 | } 127 | 128 | return 0; 129 | } 130 | 131 | int lua_register_context(lua_State *L, rb_ctx_t *ctx) { 132 | lua_pushlightuserdata(L, (void *)rb_ctx_from_lua); 133 | lua_pushlightuserdata(L, (void *)ctx); 134 | lua_settable(L, LUA_REGISTRYINDEX); 135 | return 0; 136 | } 137 | -------------------------------------------------------------------------------- /src/rootbeer_cli/src/rev_store/create.c: -------------------------------------------------------------------------------- 1 | #include "store_module.h" 2 | #include "rootbeer.h" 3 | 4 | // Generic function used to copy context files to the store path 5 | int rb_store_copy_files(rb_revision_t *rev) { 6 | char store_path[PATH_MAX]; 7 | sprintf(store_path, "%s/store/%d", STORE_ROOT, rev->id); 8 | 9 | // Assert because this is entirely a developer error 10 | assert(access(store_path, F_OK) == 0); 11 | 12 | char cfg_dir[PATH_MAX]; 13 | char ref_dir[PATH_MAX]; 14 | 15 | sprintf(cfg_dir, "%s/%s", store_path, "cfg"); 16 | sprintf(ref_dir, "%s/%s", store_path, "ref"); 17 | 18 | // Copy the config files 19 | for (int i = 0; i < rev->cfg_filesc; i++) { 20 | char src[PATH_MAX]; 21 | char dst[PATH_MAX]; 22 | 23 | sprintf(src, "%s/%s", rev->pwd, rev->cfg_filesv[i]); 24 | sprintf(dst, "%s/%s", cfg_dir, rev->cfg_filesv[i]); 25 | 26 | if (rb_copy_file(src, dst) != 0) { 27 | return 1; 28 | } 29 | } 30 | 31 | // Copy the reference files 32 | for (int i = 0; i < rev->ref_filesc; i++) { 33 | char src[PATH_MAX]; 34 | char dst[PATH_MAX]; 35 | 36 | sprintf(src, "%s/%s", rev->pwd, rev->ref_filesv[i]); 37 | sprintf(dst, "%s/%s", ref_dir, rev->ref_filesv[i]); 38 | 39 | if (rb_copy_file(src, dst) != 0) { 40 | return 1; 41 | } 42 | } 43 | 44 | return 0; 45 | } 46 | 47 | // Exports our rb_revision_t to the actual store on disk 48 | int rb_store_revision_to_disk(rb_revision_t *rev) { 49 | char store_path[PATH_MAX]; 50 | sprintf(store_path, "%s/store/%d", STORE_ROOT, rev->id); 51 | printf("Storing revision %d at %s\n", rev->id, store_path); 52 | 53 | // Assert because this is entirely a developer error 54 | assert(access(store_path, F_OK) != 0); 55 | 56 | // Create the directory for the revision 57 | if (mkdir(store_path, 0755) != 0) { 58 | return 1; 59 | } 60 | 61 | // Write the meta file 62 | char meta_path[PATH_MAX]; 63 | sprintf(meta_path, "%s/_meta", store_path); 64 | FILE *m_file = fopen(meta_path, "w"); 65 | 66 | // TODO: Debug log all these returns 67 | if (m_file == NULL) { 68 | return 1; 69 | } 70 | 71 | fprintf(m_file, "name=%s\n", rev->name); 72 | fprintf(m_file, "timestamp=%ld\n", rev->timestamp); 73 | fprintf(m_file, "cfg_files="); 74 | 75 | for (int i = 0; i < rev->cfg_filesc; i++) { 76 | fprintf(m_file, "%s", rev->cfg_filesv[i]); 77 | if (i != rev->cfg_filesc - 1) { 78 | fprintf(m_file, ","); 79 | } 80 | } 81 | 82 | fprintf(m_file, "\nref_files="); 83 | for (int i = 0; i < rev->ref_filesc; i++) { 84 | fprintf(m_file, "%s", rev->ref_filesv[i]); 85 | if (i != rev->ref_filesc - 1) { 86 | fprintf(m_file, ","); 87 | } 88 | } 89 | 90 | fprintf(m_file, "\ngen_files="); 91 | for (int i = 0; i < rev->gen_filesc; i++) { 92 | fprintf(m_file, "%s", rev->gen_filesv[i]); 93 | if (i != rev->gen_filesc - 1) { 94 | fprintf(m_file, ","); 95 | } 96 | } 97 | 98 | fprintf(m_file, "\n"); 99 | fclose(m_file); 100 | 101 | return rb_store_copy_files(rev); 102 | } 103 | 104 | int rb_store_copy_and_resolve_files(rb_ctx_t *ctx, rb_revision_t *rev) { 105 | // Resolve the entry point file and add it to the cfg files 106 | char entry_point[PATH_MAX]; 107 | realpath(ctx->script_path, entry_point); 108 | 109 | rev->cfg_filesv[0] = malloc(strlen(entry_point) - strlen(rev->pwd)); 110 | sprintf(rev->cfg_filesv[0], "%s", entry_point + strlen(rev->pwd) + 1); 111 | 112 | // Resolve all the config file paths and trim off the PWD 113 | // since we are storing the hierarchy as-is on disk 114 | for (size_t i = 1; i < ctx->lua_files_count + 1; i++) { 115 | char cfg_file[PATH_MAX]; 116 | realpath(ctx->lua_files[i - 1], cfg_file); 117 | 118 | // Trim off the PWD (which is easy since we just skip strlen(rev->pwd)) 119 | // The paths that are relative to the config path will always start 120 | // with the PWD anyways, so we just advance the array by the length 121 | rev->cfg_filesv[i] = malloc(strlen(cfg_file) - strlen(rev->pwd)); 122 | sprintf(rev->cfg_filesv[i], "%s", cfg_file + strlen(rev->pwd) + 1); 123 | } 124 | 125 | // Do the exact same logic for the reference files 126 | for (size_t i = 0; i < ctx->ext_files_count; i++) { 127 | char ref_file[PATH_MAX]; 128 | realpath(ctx->ext_files[i], ref_file); 129 | 130 | rev->ref_filesv[i] = malloc(strlen(ref_file) - strlen(rev->pwd)); 131 | sprintf(rev->ref_filesv[i], "%s", ref_file + strlen(rev->pwd) + 1); 132 | } 133 | 134 | // And the generated files 135 | for (size_t i = 0; i < ctx->plugin_transforms_count; i++) { 136 | rev->gen_filesv[i] = strdup(ctx->plugin_transforms[i]); 137 | } 138 | 139 | return 0; 140 | } 141 | 142 | // Given a context from our lua execution, convert it into an rb_revision_t 143 | // and then dump it into the store with the next available ID. 144 | int rb_store_dump_revision(rb_ctx_t *ctx) { 145 | assert(getuid() == 0); // We need to be root to dump a revision 146 | 147 | rb_revision_t *rev = malloc(sizeof(rb_revision_t)); 148 | assert(rev != NULL); 149 | assert(ctx != NULL); 150 | 151 | rev->id = rb_store_next_id(); 152 | rev->name = "test rev for now"; 153 | rev->timestamp = time(NULL); 154 | 155 | // Revisions need full paths to the files, requiring we resolve the PWD 156 | rev->pwd = malloc(PATH_MAX); 157 | assert(rev->pwd != NULL); 158 | assert(realpath(ctx->script_dir, rev->pwd) != NULL); 159 | 160 | int file_count = ctx->lua_files_count + 1; // Include the entry point file 161 | rev->cfg_filesv = malloc(sizeof(char *) * file_count); 162 | rev->cfg_filesc = file_count; 163 | 164 | int ref_count = ctx->ext_files_count; 165 | rev->ref_filesv = malloc(sizeof(char *) * ref_count); 166 | rev->ref_filesc = ref_count; 167 | 168 | int gen_count = ctx->plugin_transforms_count; 169 | rev->gen_filesv = malloc(sizeof(char *) * gen_count); 170 | rev->gen_filesc = gen_count; 171 | 172 | if (rb_store_copy_and_resolve_files(ctx, rev) != 0) { 173 | fprintf(stderr, "error: failed to copy and resolve files\n"); 174 | return 1; 175 | } 176 | 177 | if (rb_store_revision_to_disk(rev) != 0) { 178 | fprintf(stderr, "error: failed to store revision\n"); 179 | return 1; 180 | } 181 | 182 | rb_store_set_current_revision(rev->id); 183 | return 0; 184 | } 185 | --------------------------------------------------------------------------------