| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- using GLib;
- namespace Spry.Cli {
- /**
- * File-shaping helpers for the spry CLI: template substitution, marker
- * block insertion, name normalisation and recursive copy/remove.
- */
- public class Generator : Object {
- /**
- * Replaces every `{{KEY}}` placeholder in `template` with the value
- * under `KEY` in `vars`.
- */
- public static string fill(string template, HashTable<string, string> vars) {
- var result = template;
- foreach (var key in vars.get_keys()) {
- result = result.replace("{{" + key + "}}", vars.lookup(key));
- }
- return result;
- }
- /**
- * Writes `contents` to `path`, creating parent directories as
- * needed. `mode` is applied after writing — pass 0600 for files
- * carrying key material (`web-config.json`) so other local users
- * cannot read them.
- */
- public static void write_file(string path, string contents, uint32 mode = 0644) throws Error {
- var parent = File.new_for_path(path).get_parent();
- if (parent != null && !((!)parent).query_exists()) {
- try {
- ((!)parent).make_directory_with_parents(null);
- } catch (Error e) {
- if (!FileUtils.test(path, FileTest.EXISTS)) {
- throw e;
- }
- }
- }
- var stream = new DataOutputStream(File.new_for_path(path).replace(null, false, FileCreateFlags.NONE));
- stream.put_string(contents);
- stream.close();
- File.new_for_path(path).set_attribute_uint32(FileAttribute.UNIX_MODE, mode,
- FileQueryInfoFlags.NONE, null);
- }
- /** Reads `path` in one string. */
- public static string read_file(string path) throws Error {
- string contents;
- if (!FileUtils.get_contents(path, out contents)) {
- throw cli_error("cannot read %s", path);
- }
- return contents;
- }
- /**
- * Appends `section` to `path` unless a line equal to `header` is
- * already present. The file is created when absent.
- */
- public static void append_section(string path, string header, string section) throws Error {
- var contents = "";
- if (FileUtils.test(path, FileTest.EXISTS)) {
- contents = read_file(path);
- if (contents.contains(header)) {
- return;
- }
- if (contents.length > 0 && !contents.has_suffix("\n")) {
- contents += "\n";
- }
- if (contents.length > 0) {
- contents += "\n";
- }
- }
- write_file(path, contents + section.strip() + "\n");
- }
- /**
- * Inserts `snippet` between the `begin`/`end` marker lines inside
- * `contents`, returning the updated text. Insertion is idempotent:
- * when the snippet's first non-empty line already appears between the
- * markers, `contents` is returned unchanged. Snippet lines are
- * indented to match the block's existing content (or the marker line
- * itself for an empty block); surrounding lines are never touched.
- */
- public static string insert_marked(string contents, string begin, string end, string snippet) throws Error {
- string[] lines = contents.split("\n", -1);
- int begin_index = -1;
- int end_index = -1;
- for (int i = 0; i < lines.length; i++) {
- if (begin_index < 0) {
- if (lines[i].strip() == begin) {
- begin_index = i;
- }
- } else if (lines[i].strip() == end) {
- end_index = i;
- break;
- }
- }
- if (begin_index < 0 || end_index < 0) {
- throw cli_error("marker block %s … %s not found — was this file generated by spry?", begin, end);
- }
- string[] snippet_lines = snippet.split("\n", -1);
- string first = "";
- foreach (var line in snippet_lines) {
- if (line.strip().length > 0) {
- first = line.strip();
- break;
- }
- }
- string indent = "";
- for (int i = end_index - 1; i > begin_index; i--) {
- var stripped = lines[i].strip();
- if (stripped.length > 0) {
- indent = lines[i].substring(0, lines[i].length - stripped.length);
- break;
- }
- }
- if (indent.length == 0) {
- var stripped = lines[begin_index].strip();
- indent = lines[begin_index].substring(0, lines[begin_index].length - stripped.length);
- }
- for (int i = begin_index + 1; i < end_index; i++) {
- if (lines[i].strip() == first) {
- return contents;
- }
- }
- var b = new StringBuilder();
- for (int i = 0; i < end_index; i++) {
- b.append(lines[i]).append("\n");
- }
- foreach (var line in snippet_lines) {
- b.append(line.length > 0 ? indent + line : "").append("\n");
- }
- for (int i = end_index; i < lines.length; i++) {
- b.append(lines[i]);
- if (i < lines.length - 1) {
- b.append("\n");
- }
- }
- return b.str;
- }
- /** True when `name` is a valid application/page/action name. */
- public static bool valid_name(string name) {
- if (name.length == 0 || !is_letter(name.data[0])) {
- return false;
- }
- foreach (var c in name.data) {
- if (!(is_word_byte(c) || c == '-' || c == '_')) {
- return false;
- }
- }
- return true;
- }
- private static bool is_letter(uint8 c) {
- return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
- }
- private static bool is_word_byte(uint8 c) {
- return is_letter(c) || (c >= '0' && c <= '9');
- }
- /** `user-management` → `UserManagement`. */
- public static string pascal_case(string name) {
- var result = new StringBuilder();
- var capitalize = true;
- foreach (var c in name.data) {
- if (c == '-' || c == '_') {
- capitalize = true;
- } else if (capitalize) {
- result.append_c((char) (c >= 'a' && c <= 'z' ? c - 32 : c));
- capitalize = false;
- } else {
- result.append_c((char) c);
- }
- }
- return result.str;
- }
- /** `UserManagement` → `user_management`. */
- public static string snake_case(string name) {
- return name.down().replace("-", "_");
- }
- /**
- * Copies the tree at `source` to `destination`, skipping build
- * output, VCS state and SQLite stores.
- */
- public static void copy_tree(string source, string destination) throws Error {
- copy_recursive(File.new_for_path(source), File.new_for_path(destination));
- }
- /** Recursively removes the tree at `path` (including `path` itself). */
- public static void remove_tree(string path) throws Error {
- var file = File.new_for_path(path);
- if (!file.query_exists()) {
- return;
- }
- remove_recursive(file);
- }
- private static void copy_recursive(File source, File destination) throws Error {
- var type = source.query_file_type(FileQueryInfoFlags.NONE);
- if (type == FileType.DIRECTORY) {
- if (!destination.query_exists()) {
- destination.make_directory_with_parents(null);
- }
- var enumerator = source.enumerate_children("standard::name", FileQueryInfoFlags.NONE);
- FileInfo info;
- while ((info = enumerator.next_file()) != null) {
- var name = info.get_name();
- if (skipped(name)) {
- continue;
- }
- var child = File.new_for_path(Path.build_filename(source.get_path(), name));
- var child_copy = File.new_for_path(Path.build_filename(destination.get_path(), name));
- copy_recursive(child, child_copy);
- }
- } else if (type == FileType.REGULAR) {
- var parent = destination.get_parent();
- if (parent != null && !((!)parent).query_exists()) {
- ((!)parent).make_directory_with_parents(null);
- }
- source.copy(destination, FileCopyFlags.OVERWRITE);
- }
- }
- private static void remove_recursive(File file) throws Error {
- var type = file.query_file_type(FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
- if (type == FileType.DIRECTORY) {
- var enumerator = file.enumerate_children("standard::name", FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
- FileInfo info;
- while ((info = enumerator.next_file()) != null) {
- remove_recursive(File.new_for_path(Path.build_filename(file.get_path(), info.get_name())));
- }
- }
- if (file.query_exists()) {
- file.delete();
- }
- }
- private static bool skipped(string name) {
- return name == "builddir" || name == ".git" || name == ".docker-build" || name.contains(".sqlite");
- }
- /** Builds a CLI error with a printf-style message. */
- public static Error cli_error(string format, ...) {
- return new Error(Quark.from_string("spry-cli"), 0, format, va_list());
- }
- }
- /**
- * Locates the installed statum tools and prepares the environment child
- * processes need (the statum tools silently load stale system libraries
- * when LD_LIBRARY_PATH is unset).
- */
- public class Tools : Object {
- /**
- * Resolves `name` to an executable path: `--tools-dir` when given,
- * else PATH, else ~/.local/bin.
- */
- public static string resolve(string name, string? tools_dir) throws Error {
- if (tools_dir != null) {
- var path = Path.build_filename(tools_dir, name);
- if (FileUtils.test(path, FileTest.IS_EXECUTABLE)) {
- return path;
- }
- throw Generator.cli_error("%s not found in --tools-dir %s", name, (!)tools_dir);
- }
- var found = Environment.find_program_in_path(name);
- if (found != null) {
- return (!)found;
- }
- var fallback = Path.build_filename(Environment.get_home_dir(), ".local", "bin", name);
- if (FileUtils.test(fallback, FileTest.IS_EXECUTABLE)) {
- return fallback;
- }
- throw Generator.cli_error("%s not found on PATH — pass --tools-dir", name);
- }
- /**
- * The library directory spawned tools should see: the `--libdir`
- * override, else the pkg-config libdir of spry-0.2, else
- * ~/.local/lib64.
- */
- public static string default_libdir(string? libdir) {
- if (libdir != null) {
- return (!)libdir;
- }
- try {
- var launcher = new SubprocessLauncher(SubprocessFlags.STDOUT_PIPE);
- var process = launcher.spawnv({ "pkg-config", "--variable=libdir", "spry-0.2" });
- string output;
- process.communicate_utf8(null, null, out output, null);
- if (process.get_successful()) {
- var value = (output ?? "").strip();
- if (value.length > 0 && FileUtils.test(value, FileTest.IS_DIR)) {
- return value;
- }
- }
- } catch {
- }
- return Path.build_filename(Environment.get_home_dir(), ".local", "lib64");
- }
- /**
- * Points `launcher`'s LD_LIBRARY_PATH/PKG_CONFIG_PATH/XDG_DATA_DIRS
- * at the Spry stack's library and share directories so spawned
- * processes load the freshly built libraries, resolve their
- * pkg-config packages and find the installed vapis — regardless of
- * the invoking shell's environment.
- */
- public static void apply_env(SubprocessLauncher launcher, string? libdir) {
- var dir = default_libdir(libdir);
- prepend(launcher, "LD_LIBRARY_PATH", dir);
- prepend(launcher, "PKG_CONFIG_PATH", Path.build_filename(dir, "pkgconfig"));
- prepend(launcher, "XDG_DATA_DIRS", Path.build_filename(Path.get_dirname(dir), "share"));
- }
- private static void prepend(SubprocessLauncher launcher, string name, string dir) {
- var existing = Environment.get_variable(name);
- var value = existing != null && existing.length > 0
- ? dir + Path.SEARCHPATH_SEPARATOR_S + existing
- : dir;
- launcher.setenv(name, value, true);
- }
- }
- }
|