aboutsummaryrefslogtreecommitdiffstats
path: root/src/blogc-make
diff options
context:
space:
mode:
authorRafael G. Martins <rafael@rafaelmartins.eng.br>2016-12-27 02:29:54 +0100
committerRafael G. Martins <rafael@rafaelmartins.eng.br>2016-12-27 02:43:59 +0100
commit8cac2c3e3a61b64b9a9855dec413239bcec7f9d2 (patch)
tree6240cdabaa0352f08d62bbfa6de7c53f5a3f1063 /src/blogc-make
parent7bf68b0b617fb3ffa86f38fe06a49786883037f4 (diff)
downloadblogc-8cac2c3e3a61b64b9a9855dec413239bcec7f9d2.tar.gz
blogc-8cac2c3e3a61b64b9a9855dec413239bcec7f9d2.tar.bz2
blogc-8cac2c3e3a61b64b9a9855dec413239bcec7f9d2.zip
make: implemented a build tool for blogc
so, this is basically what happens when you don't have anything better to do in the christmas weekend. most of this code was written in the last 2 or 3 days. i'd like to thank the chivas brothers, the weather and my psychological problems for this achievement. on a serious note, this tool still needs a man page, more tests, and the aws lambda function should be adapted to use it instead of (or together with) make/busybox. also, while talking about aws lambda, this tool can be nicely embedded into the blogc binary, to produce a single "small" static binary for usage in lambda ;)
Diffstat (limited to 'src/blogc-make')
-rw-r--r--src/blogc-make/atom.c93
-rw-r--r--src/blogc-make/atom.h18
-rw-r--r--src/blogc-make/ctx.c172
-rw-r--r--src/blogc-make/ctx.h43
-rw-r--r--src/blogc-make/exec-native.c161
-rw-r--r--src/blogc-make/exec-native.h18
-rw-r--r--src/blogc-make/exec.c297
-rw-r--r--src/blogc-make/exec.h24
-rw-r--r--src/blogc-make/main.c125
-rw-r--r--src/blogc-make/rules.c752
-rw-r--r--src/blogc-make/rules.h35
-rw-r--r--src/blogc-make/settings.c175
-rw-r--r--src/blogc-make/settings.h30
13 files changed, 1943 insertions, 0 deletions
diff --git a/src/blogc-make/atom.c b/src/blogc-make/atom.c
new file mode 100644
index 0000000..c7f98a5
--- /dev/null
+++ b/src/blogc-make/atom.c
@@ -0,0 +1,93 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include "../common/error.h"
+#include "../common/utils.h"
+#include "settings.h"
+#include "atom.h"
+
+static const char atom_template[] =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n"
+ " <title type=\"text\">{{ SITE_TITLE }}{%% ifdef FILTER_TAG %%} - "
+ "{{ FILTER_TAG }}{%% endif %%}</title>\n"
+ " <id>{{ BASE_URL }}/%s{%% ifdef FILTER_TAG %%}/{{ FILTER_TAG }}"
+ "{%% endif %%}%s</id>\n"
+ " <updated>{{ DATE_FIRST_FORMATTED }}</updated>\n"
+ " <link href=\"{{ BASE_DOMAIN }}{{ BASE_URL }}/\" />\n"
+ " <link href=\"{{ BASE_DOMAIN }}{{ BASE_URL }}/%s{%% ifdef FILTER_TAG %%}"
+ "/{{ FILTER_TAG }}{%% endif %%}%s\" rel=\"self\" />\n"
+ " <author>\n"
+ " <name>{{ AUTHOR_NAME }}</name>\n"
+ " <email>{{ AUTHOR_EMAIL }}</email>\n"
+ " </author>\n"
+ " <subtitle type=\"text\">{{ SITE_TAGLINE }}</subtitle>\n"
+ " {%% block listing %%}\n"
+ " <entry>\n"
+ " <title type=\"text\">{{ TITLE }}</title>\n"
+ " <id>{{ BASE_URL }}/%s/{{ FILENAME }}/</id>\n"
+ " <updated>{{ DATE_FORMATTED }}</updated>\n"
+ " <published>{{ DATE_FORMATTED }}</published>\n"
+ " <link href=\"{{ BASE_DOMAIN }}{{ BASE_URL }}/%s/{{ FILENAME }}/\" />\n"
+ " <author>\n"
+ " <name>{{ AUTHOR_NAME }}</name>\n"
+ " <email>{{ AUTHOR_EMAIL }}</email>\n"
+ " </author>\n"
+ " <content type=\"html\"><![CDATA[{{ CONTENT }}]]></content>\n"
+ " </entry>\n"
+ " {%% endblock %%}\n"
+ "</feed>\n";
+
+
+char*
+bm_atom_deploy(bm_settings_t *settings, bc_error_t **err)
+{
+ if (err == NULL || *err != NULL)
+ return NULL;
+
+ // this is not really portable
+ char fname[] = "/tmp/blogc-make_XXXXXX";
+ int fd;
+ if (-1 == (fd = mkstemp(fname))) {
+ *err = bc_error_new_printf(BLOGC_MAKE_ERROR_ATOM,
+ "Failed to create temporary atom template: %s", strerror(errno));
+ return NULL;
+ }
+
+ const char *atom_prefix = bc_trie_lookup(settings->settings, "atom_prefix");
+ const char *atom_ext = bc_trie_lookup(settings->settings, "atom_ext");
+ const char *post_prefix = bc_trie_lookup(settings->settings, "post_prefix");
+
+ char *content = bc_strdup_printf(atom_template, atom_prefix, atom_ext,
+ atom_prefix, atom_ext, post_prefix, post_prefix);
+
+ if (-1 == write(fd, content, strlen(content))) {
+ *err = bc_error_new_printf(BLOGC_MAKE_ERROR_ATOM,
+ "Failed to write to temporary atom template: %s", strerror(errno));
+ free(content);
+ close(fd);
+ unlink(fname);
+ return NULL;
+ }
+
+ free(content);
+ close(fd);
+
+ return bc_strdup(fname);
+}
+
+
+void
+bm_atom_destroy(const char *fname)
+{
+ unlink(fname);
+}
diff --git a/src/blogc-make/atom.h b/src/blogc-make/atom.h
new file mode 100644
index 0000000..49ff64a
--- /dev/null
+++ b/src/blogc-make/atom.h
@@ -0,0 +1,18 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#ifndef _MAKE_ATOM_H
+#define _MAKE_ATOM_H
+
+#include "../common/error.h"
+#include "settings.h"
+
+char* bm_atom_deploy(bm_settings_t *settings, bc_error_t **err);
+void bm_atom_destroy(const char *fname);
+
+#endif /* _MAKE_ATOM_H */
diff --git a/src/blogc-make/ctx.c b/src/blogc-make/ctx.c
new file mode 100644
index 0000000..8c9cc9a
--- /dev/null
+++ b/src/blogc-make/ctx.c
@@ -0,0 +1,172 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <libgen.h>
+#include <time.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include "../common/file.h"
+#include "../common/utils.h"
+#include "atom.h"
+#include "settings.h"
+#include "ctx.h"
+
+
+bm_filectx_t*
+bm_filectx_new(bm_ctx_t *ctx, const char *filename)
+{
+ if (ctx == NULL || filename == NULL)
+ return NULL;
+
+ char *f = filename[0] == '/' ? bc_strdup(filename) :
+ bc_strdup_printf("%s/%s", ctx->root_dir, filename);
+
+ bm_filectx_t *rv = bc_malloc(sizeof(bm_filectx_t));
+ rv->path = f;
+ rv->short_path = bc_strdup(filename);
+
+ struct stat buf;
+
+ if (0 != stat(f, &buf)) {
+ struct timespec ts;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 0;
+ rv->timestamp = ts;
+ rv->readable = false;
+ }
+ else {
+ rv->timestamp = buf.st_mtim;
+ rv->readable = true;
+ }
+
+ return rv;
+}
+
+
+void
+bm_filectx_free(bm_filectx_t *fctx)
+{
+ if (fctx == NULL)
+ return;
+ free(fctx->path);
+ free(fctx->short_path);
+ free(fctx);
+}
+
+
+bm_ctx_t*
+bm_ctx_new(const char *settings_file, bc_error_t **err)
+{
+ if (settings_file == NULL || err == NULL || *err != NULL)
+ return NULL;
+
+ size_t content_len;
+ char *content = bc_file_get_contents(settings_file, true, &content_len,
+ err);
+ if (*err != NULL)
+ return NULL;
+
+ bm_settings_t *settings = bm_settings_parse(content, content_len, err);
+ if (*err != NULL) {
+ free(content);
+ return NULL;
+ }
+ free(content);
+
+ char *atom_template = bm_atom_deploy(settings, err);
+ if (*err != NULL) {
+ return NULL;
+ }
+
+ bm_ctx_t *rv = bc_malloc(sizeof(bm_ctx_t));
+ rv->settings = settings;
+
+ char *real_filename = realpath(settings_file, NULL);
+ rv->settings_fctx = bm_filectx_new(rv, real_filename);
+ rv->root_dir = bc_strdup(dirname(real_filename));
+ free(real_filename);
+
+ const char *output_dir = bc_trie_lookup(settings->settings, "output_dir");
+ rv->output_dir = output_dir[0] == '/' ? bc_strdup(output_dir) :
+ bc_strdup_printf("%s/%s", rv->root_dir, output_dir);
+
+ const char *template_dir = bc_trie_lookup(settings->settings,
+ "template_dir");
+
+ char *main_template = bc_strdup_printf("%s/%s", template_dir,
+ bc_trie_lookup(settings->settings, "main_template"));
+ rv->main_template_fctx = bm_filectx_new(rv, main_template);
+ free(main_template);
+
+ rv->atom_template_fctx = bm_filectx_new(rv, atom_template);
+ free(atom_template);
+
+ const char *content_dir = bc_trie_lookup(settings->settings, "content_dir");
+ const char *post_prefix = bc_trie_lookup(settings->settings, "post_prefix");
+ const char *source_ext = bc_trie_lookup(settings->settings, "source_ext");
+
+ rv->posts_fctx = NULL;
+ if (settings->posts != NULL) {
+ for (size_t i = 0; settings->posts[i] != NULL; i++) {
+ char *f = bc_strdup_printf("%s/%s/%s%s", content_dir, post_prefix,
+ settings->posts[i], source_ext);
+ rv->posts_fctx = bc_slist_append(rv->posts_fctx,
+ bm_filectx_new(rv, f));
+ free(f);
+ }
+ }
+
+ rv->pages_fctx = NULL;
+ if (settings->pages != NULL) {
+ for (size_t i = 0; settings->pages[i] != NULL; i++) {
+ char *f = bc_strdup_printf("%s/%s%s", content_dir,
+ settings->pages[i], source_ext);
+ rv->pages_fctx = bc_slist_append(rv->pages_fctx,
+ bm_filectx_new(rv, f));
+ free(f);
+ }
+ }
+
+ rv->copy_files_fctx = NULL;
+ if (settings->copy_files != NULL) {
+ for (size_t i = 0; settings->copy_files[i] != NULL; i++) {
+ rv->copy_files_fctx = bc_slist_append(rv->copy_files_fctx,
+ bm_filectx_new(rv, settings->copy_files[i]));
+ }
+ }
+
+ return rv;
+}
+
+
+void
+bm_ctx_free(bm_ctx_t *ctx)
+{
+ if (ctx == NULL)
+ return;
+
+ bm_settings_free(ctx->settings);
+
+ free(ctx->root_dir);
+ free(ctx->output_dir);
+
+ bm_atom_destroy(ctx->atom_template_fctx->path);
+
+ bm_filectx_free(ctx->main_template_fctx);
+ bm_filectx_free(ctx->atom_template_fctx);
+ bm_filectx_free(ctx->settings_fctx);
+
+ bc_slist_free_full(ctx->posts_fctx, (bc_free_func_t) bm_filectx_free);
+ bc_slist_free_full(ctx->pages_fctx, (bc_free_func_t) bm_filectx_free);
+ bc_slist_free_full(ctx->copy_files_fctx, (bc_free_func_t) bm_filectx_free);
+
+ free(ctx);
+}
diff --git a/src/blogc-make/ctx.h b/src/blogc-make/ctx.h
new file mode 100644
index 0000000..3e18048
--- /dev/null
+++ b/src/blogc-make/ctx.h
@@ -0,0 +1,43 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#ifndef _MAKE_CTX_H
+#define _MAKE_CTX_H
+
+#include <stdbool.h>
+#include <time.h>
+#include "settings.h"
+
+typedef struct {
+ char *path;
+ char *short_path;
+ struct timespec timestamp;
+ bool readable;
+} bm_filectx_t;
+
+typedef struct {
+ bm_settings_t *settings;
+
+ char *root_dir;
+ char *output_dir;
+
+ bm_filectx_t *main_template_fctx;
+ bm_filectx_t *atom_template_fctx;
+ bm_filectx_t *settings_fctx;
+
+ bc_slist_t *posts_fctx;
+ bc_slist_t *pages_fctx;
+ bc_slist_t *copy_files_fctx;
+} bm_ctx_t;
+
+bm_filectx_t* bm_filectx_new(bm_ctx_t *ctx, const char *filename);
+void bm_filectx_free(bm_filectx_t *fctx);
+bm_ctx_t* bm_ctx_new(const char *filename, bc_error_t **err);
+void bm_ctx_free(bm_ctx_t *ctx);
+
+#endif /* _MAKE_CTX_H */
diff --git a/src/blogc-make/exec-native.c b/src/blogc-make/exec-native.c
new file mode 100644
index 0000000..32874b4
--- /dev/null
+++ b/src/blogc-make/exec-native.c
@@ -0,0 +1,161 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <libgen.h>
+#include <errno.h>
+#include "../common/file.h"
+#include "../common/utils.h"
+#include "exec-native.h"
+#include "ctx.h"
+
+
+int
+bm_exec_native_cp(bm_filectx_t *source, bm_filectx_t *dest, bool verbose)
+{
+ if (verbose)
+ printf("Copying '%s' to '%s'\n", source->path, dest->path);
+ else
+ printf(" COPY %s\n", dest->short_path);
+ fflush(stdout);
+
+ char *fname = bc_strdup(dest->path);
+ for (char *tmp = fname; *tmp != '\0'; tmp++) {
+ if (*tmp != '/' && *tmp != '\\')
+ continue;
+ char bkp = *tmp;
+ *tmp = '\0';
+ if ((strlen(fname) > 0) &&
+ (-1 == mkdir(fname, 0777)) &&
+ (errno != EEXIST))
+ {
+ fprintf(stderr, "blogc-make: error: failed to create output "
+ "directory (%s): %s\n", fname, strerror(errno));
+ free(fname);
+ exit(2);
+ }
+ *tmp = bkp;
+ }
+ free(fname);
+
+ int fd_from = open(source->path, O_RDONLY);
+ if (fd_from < 0) {
+ fprintf(stderr, "blogc-make: error: failed to open source file to copy "
+ " (%s): %s\n", source->path, strerror(errno));
+ return 3;
+ }
+
+ int fd_to = open(dest->path, O_WRONLY | O_CREAT, 0666);
+ if (fd_to < 0) {
+ fprintf(stderr, "blogc-make: error: failed to open destination file to "
+ "copy (%s): %s\n", dest->path, strerror(errno));
+ close(fd_from);
+ return 3;
+ }
+
+ ssize_t nread;
+ char buffer[BC_FILE_CHUNK_SIZE];
+ while (0 < (nread = read(fd_from, buffer, BC_FILE_CHUNK_SIZE))) {
+ char *out_ptr = buffer;
+ do {
+ ssize_t nwritten = write(fd_to, out_ptr, nread);
+ if (nwritten == -1) {
+ fprintf(stderr, "blogc-make: error: failed to write to "
+ "destination file (%s): %s\n", dest->path, strerror(errno));
+ close(fd_from);
+ close(fd_to);
+ return 3;
+ }
+ nread -= nwritten;
+ out_ptr += nwritten;
+ } while (nread > 0);
+ }
+
+ return 0;
+}
+
+
+int
+bm_exec_native_rm(bm_filectx_t *dest, bool verbose)
+{
+ if (verbose)
+ printf("Removing file '%s'\n", dest->path);
+ else
+ printf(" CLEAN %s\n", dest->short_path);
+ fflush(stdout);
+
+ if (0 != unlink(dest->path)) {
+ fprintf(stderr, "blogc-make: error: failed to remove file (%s): %s\n",
+ dest->path, strerror(errno));
+ return 3;
+ }
+
+ int rv = 0;
+
+ char *short_path = bc_strdup(dest->short_path);
+ char *path = bc_strdup(dest->path);
+
+ char *dir_short = dirname(short_path);
+ char *dir = dirname(path);
+
+ while (0 != strcmp(dir_short, ".")) {
+
+ DIR *d = opendir(dir);
+ if (d == NULL) {
+ fprintf(stderr, "error: failed to open directory (%s): %s\n",
+ dir, strerror(errno));
+ rv = 3;
+ break;
+ }
+
+ struct dirent *e;
+ size_t count = 0;
+ while (NULL != (e = readdir(d))) {
+ if ((0 == strcmp(e->d_name, ".")) || (0 == strcmp(e->d_name, "..")))
+ continue;
+ count++;
+ break;
+ }
+
+ if (0 != closedir(d)) {
+ fprintf(stderr, "error: failed to close directory (%s): %s\n",
+ dir, strerror(errno));
+ rv = 3;
+ break;
+ }
+
+ if (count == 0) {
+ if (verbose) {
+ printf("Removing directory '%s'\n", dir);
+ fflush(stdout);
+ }
+ if (0 != rmdir(dir)) {
+ fprintf(stderr, "error: failed to remove directory(%s): %s\n",
+ dir, strerror(errno));
+ rv = 3;
+ break;
+ }
+ }
+
+ dir_short = dirname(dir_short);
+ dir = dirname(dir);
+ }
+
+ free(short_path);
+ free(path);
+
+ return rv;
+}
diff --git a/src/blogc-make/exec-native.h b/src/blogc-make/exec-native.h
new file mode 100644
index 0000000..a83b510
--- /dev/null
+++ b/src/blogc-make/exec-native.h
@@ -0,0 +1,18 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#ifndef _MAKE_EXEC_NATIVE_H
+#define _MAKE_EXEC_NATIVE_H
+
+#include <stdbool.h>
+#include "ctx.h"
+
+int bm_exec_native_cp(bm_filectx_t *source, bm_filectx_t *dest, bool verbose);
+int bm_exec_native_rm(bm_filectx_t *dest, bool verbose);
+
+#endif /* _MAKE_EXEC_NATIVE_H */
diff --git a/src/blogc-make/exec.c b/src/blogc-make/exec.c
new file mode 100644
index 0000000..75b7c00
--- /dev/null
+++ b/src/blogc-make/exec.c
@@ -0,0 +1,297 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2015-2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <errno.h>
+#include <libgen.h>
+#include "../common/error.h"
+#include "../common/file.h"
+#include "../common/utils.h"
+#include "ctx.h"
+#include "exec.h"
+#include "settings.h"
+
+
+int
+bm_exec_command(const char *cmd, const char *input, char **output,
+ char **error, bc_error_t **err)
+{
+ if (err == NULL || *err != NULL)
+ return 3;
+
+ int fd_in[2];
+ if (-1 == pipe(fd_in)) {
+ *err = bc_error_new_printf(BLOGC_MAKE_ERROR_EXEC,
+ "Failed to create stdin pipe: %s", strerror(errno));
+ return 3;
+ }
+
+ int fd_out[2];
+ if (-1 == pipe(fd_out)) {
+ *err = bc_error_new_printf(BLOGC_MAKE_ERROR_EXEC,
+ "Failed to create stdout pipe: %s", strerror(errno));
+ close(fd_in[0]);
+ close(fd_in[1]);
+ return 3;
+ }
+
+ int fd_err[2];
+ if (-1 == pipe(fd_err)) {
+ *err = bc_error_new_printf(BLOGC_MAKE_ERROR_EXEC,
+ "Failed to create stderr pipe: %s", strerror(errno));
+ close(fd_in[0]);
+ close(fd_in[1]);
+ close(fd_out[0]);
+ close(fd_out[1]);
+ return 3;
+ }
+
+ pid_t pid = fork();
+ if (pid == -1) {
+ *err = bc_error_new_printf(BLOGC_MAKE_ERROR_EXEC,
+ "Failed to fork: %s", strerror(errno));
+ close(fd_in[0]);
+ close(fd_in[1]);
+ close(fd_out[0]);
+ close(fd_out[1]);
+ close(fd_err[0]);
+ close(fd_err[1]);
+ return 3;
+ }
+
+ // child
+ if (pid == 0) {
+ close(fd_in[1]);
+ close(fd_out[0]);
+ close(fd_err[0]);
+
+ dup2(fd_in[0], STDIN_FILENO);
+ dup2(fd_out[1], STDOUT_FILENO);
+ dup2(fd_err[1], STDERR_FILENO);
+
+ char *const argv[] = {
+ "/bin/sh",
+ "-c",
+ (char*) cmd,
+ NULL,
+ };
+
+ execv(argv[0], argv);
+
+ exit(1);
+ }
+
+ // parent
+ close(fd_in[0]);
+ close(fd_out[1]);
+ close(fd_err[1]);
+
+ if (input != NULL) {
+ if (-1 == write(fd_in[1], input, strlen(input))) {
+ *err = bc_error_new_printf(BLOGC_MAKE_ERROR_EXEC,
+ "Failed to write to stdin pipe: %s", strerror(errno));
+ close(fd_in[1]);
+ close(fd_out[0]);
+ close(fd_err[0]);
+ return 3;
+ }
+ }
+
+ close(fd_in[1]);
+
+ char buffer[BC_FILE_CHUNK_SIZE];
+ ssize_t s;
+
+ bc_string_t *out = NULL;
+ while(0 != (s = read(fd_out[0], buffer, BC_FILE_CHUNK_SIZE))) {
+ if (s == -1) {
+ *err = bc_error_new_printf(BLOGC_MAKE_ERROR_EXEC,
+ "Failed to read from stdout pipe: %s", strerror(errno));
+ close(fd_out[0]);
+ close(fd_err[0]);
+ bc_string_free(out, true);
+ return 3;
+ }
+ if (out == NULL) {
+ out = bc_string_new();
+ }
+ bc_string_append_len(out, buffer, s);
+ }
+ if (out != NULL) {
+ *output = bc_string_free(out, false);
+ }
+ close(fd_out[0]);
+
+ out = NULL;
+ while(0 != (s = read(fd_err[0], buffer, BC_FILE_CHUNK_SIZE))) {
+ if (s == -1) {
+ *err = bc_error_new_printf(BLOGC_MAKE_ERROR_EXEC,
+ "Failed to read from stderr pipe: %s", strerror(errno));
+ close(fd_err[0]);
+ bc_string_free(out, true);
+ return 3;
+ }
+ if (out == NULL)
+ out = bc_string_new();
+ bc_string_append_len(out, buffer, s);
+ }
+ if (out != NULL) {
+ *error = bc_string_free(out, false);
+ }
+ close(fd_err[0]);
+
+ int status;
+ waitpid(pid, &status, 0);
+
+ return WEXITSTATUS(status);
+}
+
+
+static void
+list_variables(const char *key, const char *value, bc_string_t *str)
+{
+ char *tmp = bc_shell_quote(value);
+ bc_string_append_printf(str, " -D %s=%s", key, tmp);
+ free(tmp);
+}
+
+
+char*
+bm_exec_build_blogc_cmd(bm_settings_t *settings, bc_trie_t *variables,
+ bool listing, const char *template, const char *output, bool sources_stdin)
+{
+ bc_string_t *rv = bc_string_new();
+
+ const char *locale = NULL;
+ if (settings != NULL) {
+ locale = bc_trie_lookup(settings->settings, "locale");
+ }
+ if (locale != NULL) {
+ char *tmp = bc_shell_quote(locale);
+ bc_string_append_printf(rv, "LC_ALL=%s ", tmp);
+ free(tmp);
+ }
+
+ bc_string_append(rv, "blogc");
+
+ if (settings != NULL) {
+ bc_trie_foreach(settings->env,
+ (bc_trie_foreach_func_t) list_variables, rv);
+ }
+
+ bc_trie_foreach(variables, (bc_trie_foreach_func_t) list_variables, rv);
+
+ if (listing) {
+ bc_string_append(rv, " -l");
+ }
+
+ if (template != NULL) {
+ char *tmp = bc_shell_quote(template);
+ bc_string_append_printf(rv, " -t %s", tmp);
+ free(tmp);
+ }
+
+ if (output != NULL) {
+ char *tmp = bc_shell_quote(output);
+ bc_string_append_printf(rv, " -o %s", tmp);
+ free(tmp);
+ }
+
+ if (sources_stdin) {
+ bc_string_append(rv, " -i");
+ }
+
+ return bc_string_free(rv, false);
+}
+
+
+int
+bm_exec_blogc(bm_settings_t *settings, bc_trie_t *variables, bool listing,
+ bm_filectx_t *template, bm_filectx_t *output, bc_slist_t *sources,
+ bool verbose, bool only_first_source)
+{
+ bc_string_t *input = bc_string_new();
+ for (bc_slist_t *l = sources; l != NULL; l = l->next) {
+ bc_string_append_printf(input, "%s\n", ((bm_filectx_t*) l->data)->path);
+ if (only_first_source)
+ break;
+ }
+
+ char *cmd = bm_exec_build_blogc_cmd(settings, variables, listing,
+ template->path, output->path, input->len > 0);
+
+ if (verbose)
+ printf("%s\n", cmd);
+ else
+ printf(" BLOGC %s\n", output->short_path);
+ fflush(stdout);
+
+ char *out = NULL;
+ char *err = NULL;
+ bc_error_t *error = NULL;
+
+ int rv = bm_exec_command(cmd, input->str, &out, &err, &error);
+
+ if (error != NULL) {
+ bc_error_print(error, "blogc-make");
+ free(cmd);
+ free(out);
+ free(err);
+ bc_string_free(input, true);
+ bc_error_free(error);
+ return 3;
+ }
+
+ if (rv != 0) {
+ if (verbose) {
+ fprintf(stderr,
+ "error: Failed to execute command.\n"
+ "\n"
+ "STATUS CODE: %d\n", rv);
+ if (input->len > 0) {
+ fprintf(stderr, "\nSTDIN:\n"
+ "----------------------------->8-----------------------------\n"
+ "%s\n"
+ "----------------------------->8-----------------------------\n",
+ bc_str_strip(input->str));
+ }
+ if (out != NULL) {
+ fprintf(stderr, "\nSTDOUT:\n"
+ "----------------------------->8-----------------------------\n"
+ "%s\n"
+ "----------------------------->8-----------------------------\n",
+ bc_str_strip(out));
+ }
+ if (err != NULL) {
+ fprintf(stderr, "\nSTDERR:\n"
+ "----------------------------->8-----------------------------\n"
+ "%s\n"
+ "----------------------------->8-----------------------------\n",
+ bc_str_strip(err));
+ }
+ }
+ else {
+ fprintf(stderr,
+ "error: Failed to execute command, returned status code: %d\n",
+ rv);
+ }
+ }
+
+ bc_string_free(input, true);
+ free(cmd);
+ free(out);
+ free(err);
+
+ return rv;
+}
diff --git a/src/blogc-make/exec.h b/src/blogc-make/exec.h
new file mode 100644
index 0000000..907109a
--- /dev/null
+++ b/src/blogc-make/exec.h
@@ -0,0 +1,24 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2015-2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#ifndef _MAKE_EXEC_H
+#define _MAKE_EXEC_H
+
+#include "../common/error.h"
+#include "../common/utils.h"
+#include "settings.h"
+
+int bm_exec_command(const char *cmd, const char *input, char **output,
+ char **error, bc_error_t **err);
+char* bm_exec_build_blogc_cmd(bm_settings_t *settings, bc_trie_t *variables,
+ bool listing, const char *template, const char *output, bool sources_stdin);
+int bm_exec_blogc(bm_settings_t *settings, bc_trie_t *variables, bool listing,
+ bm_filectx_t *template, bm_filectx_t *output, bc_slist_t *sources,
+ bool verbose, bool only_first_source);
+
+#endif /* _MAKE_EXEC_H */
diff --git a/src/blogc-make/main.c b/src/blogc-make/main.c
new file mode 100644
index 0000000..bc8c926
--- /dev/null
+++ b/src/blogc-make/main.c
@@ -0,0 +1,125 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2015-2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /* HAVE_CONFIG_H */
+
+#include <locale.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "../common/error.h"
+#include "../common/utils.h"
+#include "ctx.h"
+#include "rules.h"
+
+
+static void
+print_help(void)
+{
+ printf(
+ "usage:\n"
+ " blogc-make [-h] [-v] [-V] [-f FILE] [RULE ...]\n"
+ " - A simple build tool for blogc.\n"
+ "\n"
+ "positional arguments:\n"
+ " RULE build rule(s) to run (default: all)\n"
+ "\n"
+ "optional arguments:\n"
+ " -h show this help message and exit\n"
+ " -v show version and exit\n"
+ " -V be verbose when executing commands\n"
+ " -f FILE settings file (default: settings.ini)\n");
+ bm_rule_print_help();
+}
+
+
+static void
+print_usage(void)
+{
+ printf("usage: blogc-make [-h] [-v] [-V] [-f FILE] [RULE ...]\n");
+}
+
+
+int
+#ifdef MAKE_EMBEDDED
+bm_main(int argc, char **argv)
+#else
+main(int argc, char **argv)
+#endif
+{
+ setlocale(LC_ALL, "");
+
+ int rv = 0;
+ bc_error_t *err = NULL;
+
+ bc_slist_t *rules = NULL;
+ bool verbose = false;
+ char *settings_file = NULL;
+ bm_ctx_t *ctx = NULL;
+
+ for (unsigned int i = 1; i < argc; i++) {
+ if (argv[i][0] == '-') {
+ switch (argv[i][1]) {
+ case 'h':
+ print_help();
+ goto cleanup;
+ case 'v':
+ printf("%s\n", PACKAGE_STRING);
+ goto cleanup;
+ case 'V':
+ verbose = true;
+ break;
+ case 'f':
+ if (argv[i][2] != '\0')
+ settings_file = bc_strdup(argv[i] + 2);
+ else if (i + 1 < argc)
+ settings_file = bc_strdup(argv[++i]);
+ break;
+#ifdef MAKE_EMBEDDED
+ case 'm':
+ // no-op, for embedding into blogc binary.
+ break;
+#endif
+ default:
+ print_usage();
+ fprintf(stderr, "blogc-make: error: invalid argument: "
+ "-%c\n", argv[i][1]);
+ rv = 3;
+ goto cleanup;
+ }
+ }
+ else {
+ rules = bc_slist_append(rules, bc_strdup(argv[i]));
+ }
+ }
+
+ if (rules == NULL) {
+ rules = bc_slist_append(rules, bc_strdup("all"));
+ }
+
+ ctx = bm_ctx_new(settings_file ? settings_file : "settings.ini", &err);
+ if (err != NULL) {
+ bc_error_print(err, "blogc-make");
+ rv = 3;
+ goto cleanup;
+ }
+
+ rv = bm_rule_executor(ctx, rules, verbose);
+
+cleanup:
+
+ bc_slist_free_full(rules, free);
+ free(settings_file);
+ bm_ctx_free(ctx);
+ bc_error_free(err);
+
+ return rv;
+}
diff --git a/src/blogc-make/rules.c b/src/blogc-make/rules.c
new file mode 100644
index 0000000..80de6bb
--- /dev/null
+++ b/src/blogc-make/rules.c
@@ -0,0 +1,752 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <math.h>
+#include "../common/utils.h"
+#include "ctx.h"
+#include "exec.h"
+#include "exec-native.h"
+#include "rules.h"
+
+
+// INDEX RULE
+
+static bc_slist_t*
+index_outputlist(bm_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL)
+ return NULL;
+
+ bc_slist_t *rv = NULL;
+ const char *html_ext = bc_trie_lookup(ctx->settings->settings,
+ "html_ext");
+ const char *output_dir = bc_trie_lookup(ctx->settings->settings,
+ "output_dir");
+ const char *index_prefix = bc_trie_lookup(ctx->settings->settings,
+ "index_prefix");
+ bool is_index = (index_prefix == NULL) && (html_ext[0] == '/');
+ char *f = bc_strdup_printf("%s%s%s%s", output_dir,
+ is_index ? "" : "/", is_index ? "" : index_prefix,
+ html_ext);
+ rv = bc_slist_append(rv, bm_filectx_new(ctx, f));
+ free(f);
+ return rv;
+}
+
+static int
+index_exec(bm_ctx_t *ctx, bc_slist_t *outputs, bool verbose)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL)
+ return 0;
+
+ int rv = 0;
+
+ bc_trie_t *variables = bc_trie_new(free);
+ bc_trie_insert(variables, "FILTER_PER_PAGE",
+ bc_strdup(bc_trie_lookup(ctx->settings->settings, "posts_per_page")));
+ bc_trie_insert(variables, "FILTER_PAGE", bc_strdup("1"));
+ bc_trie_insert(variables, "DATE_FORMAT",
+ bc_strdup(bc_trie_lookup(ctx->settings->settings, "date_format")));
+ bc_trie_insert(variables, "BM_RULE", bc_strdup("index"));
+ bc_trie_insert(variables, "BM_TYPE", bc_strdup("post"));
+
+ for (bc_slist_t *l = outputs; l != NULL; l = l->next) {
+ bm_filectx_t *fctx = l->data;
+ if (fctx == NULL)
+ continue;
+ if (bm_rule_need_rebuild(ctx->posts_fctx, ctx->settings_fctx,
+ ctx->main_template_fctx, fctx, false))
+ {
+ rv = bm_exec_blogc(ctx->settings, variables, true,
+ ctx->main_template_fctx, fctx, ctx->posts_fctx, verbose,
+ false);
+ if (rv != 0)
+ break;
+ }
+ }
+
+ bc_trie_free(variables);
+
+ return rv;
+}
+
+
+// ATOM RULE
+
+static bc_slist_t*
+atom_outputlist(bm_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL)
+ return NULL;
+
+ bc_slist_t *rv = NULL;
+ const char *output_dir = bc_trie_lookup(ctx->settings->settings,
+ "output_dir");
+ const char *atom_prefix = bc_trie_lookup(ctx->settings->settings,
+ "atom_prefix");
+ const char *atom_ext = bc_trie_lookup(ctx->settings->settings, "atom_ext");
+ char *f = bc_strdup_printf("%s/%s%s", output_dir, atom_prefix, atom_ext);
+ rv = bc_slist_append(rv, bm_filectx_new(ctx, f));
+ free(f);
+ return rv;
+}
+
+static int
+atom_exec(bm_ctx_t *ctx, bc_slist_t *outputs, bool verbose)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL)
+ return 0;
+
+ int rv = 0;
+
+ bc_trie_t *variables = bc_trie_new(free);
+ bc_trie_insert(variables, "FILTER_PER_PAGE",
+ bc_strdup(bc_trie_lookup(ctx->settings->settings,
+ "atom_posts_per_page")));
+ bc_trie_insert(variables, "FILTER_PAGE", bc_strdup("1"));
+ bc_trie_insert(variables, "DATE_FORMAT", bc_strdup("%Y-%m-%dT%H:%M:%SZ"));
+ bc_trie_insert(variables, "BM_RULE", bc_strdup("atom"));
+ bc_trie_insert(variables, "BM_TYPE", bc_strdup("atom"));
+
+ for (bc_slist_t *l = outputs; l != NULL; l = l->next) {
+ bm_filectx_t *fctx = l->data;
+ if (fctx == NULL)
+ continue;
+ if (bm_rule_need_rebuild(ctx->posts_fctx, ctx->settings_fctx, NULL,
+ fctx, false))
+ {
+ rv = bm_exec_blogc(ctx->settings, variables, true,
+ ctx->atom_template_fctx, fctx, ctx->posts_fctx, verbose,
+ false);
+ if (rv != 0)
+ break;
+ }
+ }
+
+ bc_trie_free(variables);
+
+ return rv;
+}
+
+
+// ATOM TAGS RULE
+
+static bc_slist_t*
+atom_tags_outputlist(bm_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL || ctx->settings->tags == NULL)
+ return NULL;
+
+ bc_slist_t *rv = NULL;
+ const char *output_dir = bc_trie_lookup(ctx->settings->settings,
+ "output_dir");
+ const char *atom_prefix = bc_trie_lookup(ctx->settings->settings,
+ "atom_prefix");
+ const char *atom_ext = bc_trie_lookup(ctx->settings->settings, "atom_ext");
+ for (size_t i = 0; ctx->settings->tags[i] != NULL; i++) {
+ char *f = bc_strdup_printf("%s/%s/%s%s", output_dir, atom_prefix,
+ ctx->settings->tags[i], atom_ext);
+ rv = bc_slist_append(rv, bm_filectx_new(ctx, f));
+ free(f);
+ }
+ return rv;
+}
+
+static int
+atom_tags_exec(bm_ctx_t *ctx, bc_slist_t *outputs, bool verbose)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL || ctx->settings->tags == NULL)
+ return 0;
+
+ int rv = 0;
+ size_t i = 0;
+
+ bc_trie_t *variables = bc_trie_new(free);
+ bc_trie_insert(variables, "FILTER_PER_PAGE",
+ bc_strdup(bc_trie_lookup(ctx->settings->settings,
+ "atom_posts_per_page")));
+ bc_trie_insert(variables, "FILTER_PAGE", bc_strdup("1"));
+ bc_trie_insert(variables, "DATE_FORMAT", bc_strdup("%Y-%m-%dT%H:%M:%SZ"));
+ bc_trie_insert(variables, "BM_RULE", bc_strdup("atom_tags"));
+ bc_trie_insert(variables, "BM_TYPE", bc_strdup("atom"));
+
+ for (bc_slist_t *l = outputs; l != NULL; l = l->next, i++) {
+ bm_filectx_t *fctx = l->data;
+ if (fctx == NULL)
+ continue;
+
+ bc_trie_insert(variables, "FILTER_TAG",
+ bc_strdup(ctx->settings->tags[i]));
+
+ if (bm_rule_need_rebuild(ctx->posts_fctx, ctx->settings_fctx, NULL,
+ fctx, false))
+ {
+ rv = bm_exec_blogc(ctx->settings, variables, true,
+ ctx->atom_template_fctx, fctx, ctx->posts_fctx, verbose,
+ false);
+ if (rv != 0)
+ break;
+ }
+ }
+
+ bc_trie_free(variables);
+
+ return rv;
+}
+
+
+// PAGINATION RULE
+
+static bc_slist_t*
+pagination_outputlist(bm_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL)
+ return NULL;
+
+ long num_posts = bc_slist_length(ctx->posts_fctx);
+ long posts_per_page = strtol(
+ bc_trie_lookup(ctx->settings->settings, "posts_per_page"),
+ NULL, 10); // FIXME: improve
+ size_t pages = ceilf(((float) num_posts) / posts_per_page);
+
+ const char *output_dir = bc_trie_lookup(ctx->settings->settings,
+ "output_dir");
+ const char *pagination_prefix = bc_trie_lookup(ctx->settings->settings,
+ "pagination_prefix");
+ const char *html_ext = bc_trie_lookup(ctx->settings->settings,
+ "html_ext");
+
+ bc_slist_t *rv = NULL;
+ for (size_t i = 0; i < pages; i++) {
+ char *f = bc_strdup_printf("%s/%s/%d%s", output_dir, pagination_prefix,
+ i + 1, html_ext);
+ rv = bc_slist_append(rv, bm_filectx_new(ctx, f));
+ free(f);
+ }
+ return rv;
+}
+
+static int
+pagination_exec(bm_ctx_t *ctx, bc_slist_t *outputs, bool verbose)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL)
+ return 0;
+
+ int rv = 0;
+ size_t page = 1;
+
+ bc_trie_t *variables = bc_trie_new(free);
+ bc_trie_insert(variables, "FILTER_PER_PAGE",
+ bc_strdup(bc_trie_lookup(ctx->settings->settings, "posts_per_page")));
+ bc_trie_insert(variables, "DATE_FORMAT",
+ bc_strdup(bc_trie_lookup(ctx->settings->settings, "date_format")));
+ bc_trie_insert(variables, "BM_RULE", bc_strdup("pagination"));
+ bc_trie_insert(variables, "BM_TYPE", bc_strdup("post"));
+
+ for (bc_slist_t *l = outputs; l != NULL; l = l->next, page++) {
+ bm_filectx_t *fctx = l->data;
+ if (fctx == NULL)
+ continue;
+ bc_trie_insert(variables, "FILTER_PAGE", bc_strdup_printf("%zu", page));
+ if (bm_rule_need_rebuild(ctx->posts_fctx, ctx->settings_fctx,
+ ctx->main_template_fctx, fctx, false))
+ {
+ rv = bm_exec_blogc(ctx->settings, variables, true,
+ ctx->main_template_fctx, fctx, ctx->posts_fctx, verbose, false);
+ if (rv != 0)
+ break;
+ }
+ }
+
+ bc_trie_free(variables);
+
+ return rv;
+}
+
+
+// POSTS RULE
+
+static bc_slist_t*
+posts_outputlist(bm_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL)
+ return NULL;
+
+ const char *output_dir = bc_trie_lookup(ctx->settings->settings,
+ "output_dir");
+ const char *post_prefix = bc_trie_lookup(ctx->settings->settings,
+ "post_prefix");
+ const char *html_ext = bc_trie_lookup(ctx->settings->settings,
+ "html_ext");
+
+ bc_slist_t *rv = NULL;
+ for (size_t i = 0; ctx->settings->posts[i] != NULL; i++) {
+ char *f = bc_strdup_printf("%s/%s/%s%s", output_dir, post_prefix,
+ ctx->settings->posts[i], html_ext);
+ rv = bc_slist_append(rv, bm_filectx_new(ctx, f));
+ free(f);
+ }
+ return rv;
+}
+
+static int
+posts_exec(bm_ctx_t *ctx, bc_slist_t *outputs, bool verbose)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL)
+ return 0;
+
+ int rv = 0;
+
+ bc_trie_t *variables = bc_trie_new(free);
+ bc_trie_insert(variables, "IS_POST", bc_strdup("1"));
+ bc_trie_insert(variables, "DATE_FORMAT",
+ bc_strdup(bc_trie_lookup(ctx->settings->settings, "date_format")));
+ bc_trie_insert(variables, "BM_RULE", bc_strdup("posts"));
+ bc_trie_insert(variables, "BM_TYPE", bc_strdup("post"));
+
+ bc_slist_t *s, *o;
+
+ for (s = ctx->posts_fctx, o = outputs; s != NULL && o != NULL;
+ s = s->next, o = o->next)
+ {
+ bm_filectx_t *o_fctx = o->data;
+ if (o_fctx == NULL)
+ continue;
+ if (bm_rule_need_rebuild(s, ctx->settings_fctx,
+ ctx->main_template_fctx, o_fctx, true))
+ {
+ rv = bm_exec_blogc(ctx->settings, variables, false,
+ ctx->main_template_fctx, o_fctx, s, verbose, true);
+ if (rv != 0)
+ break;
+ }
+ }
+
+ bc_trie_free(variables);
+
+ return rv;
+}
+
+
+// TAGS RULE
+
+static bc_slist_t*
+tags_outputlist(bm_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL || ctx->settings->tags == NULL)
+ return NULL;
+
+ bc_slist_t *rv = NULL;
+ const char *output_dir = bc_trie_lookup(ctx->settings->settings,
+ "output_dir");
+ const char *tag_prefix = bc_trie_lookup(ctx->settings->settings,
+ "tag_prefix");
+ const char *html_ext = bc_trie_lookup(ctx->settings->settings, "html_ext");
+ for (size_t i = 0; ctx->settings->tags[i] != NULL; i++) {
+ char *f = bc_strdup_printf("%s/%s/%s%s", output_dir, tag_prefix,
+ ctx->settings->tags[i], html_ext);
+ rv = bc_slist_append(rv, bm_filectx_new(ctx, f));
+ free(f);
+ }
+ return rv;
+}
+
+static int
+tags_exec(bm_ctx_t *ctx, bc_slist_t *outputs, bool verbose)
+{
+ if (ctx == NULL || ctx->settings->posts == NULL || ctx->settings->tags == NULL)
+ return 0;
+
+ int rv = 0;
+ size_t i = 0;
+
+ bc_trie_t *variables = bc_trie_new(free);
+ bc_trie_insert(variables, "FILTER_PER_PAGE",
+ bc_strdup(bc_trie_lookup(ctx->settings->settings,
+ "atom_posts_per_page")));
+ bc_trie_insert(variables, "FILTER_PAGE", bc_strdup("1"));
+ bc_trie_insert(variables, "DATE_FORMAT",
+ bc_strdup(bc_trie_lookup(ctx->settings->settings, "date_format")));
+ bc_trie_insert(variables, "BM_RULE", bc_strdup("tags"));
+ bc_trie_insert(variables, "BM_TYPE", bc_strdup("post"));
+
+ for (bc_slist_t *l = outputs; l != NULL; l = l->next, i++) {
+ bm_filectx_t *fctx = l->data;
+ if (fctx == NULL)
+ continue;
+
+ bc_trie_insert(variables, "FILTER_TAG",
+ bc_strdup(ctx->settings->tags[i]));
+
+ if (bm_rule_need_rebuild(ctx->posts_fctx, ctx->settings_fctx,
+ ctx->main_template_fctx, fctx, false))
+ {
+ rv = bm_exec_blogc(ctx->settings, variables, true,
+ ctx->main_template_fctx, fctx, ctx->posts_fctx, verbose,
+ false);
+ if (rv != 0)
+ break;
+ }
+ }
+
+ bc_trie_free(variables);
+
+ return rv;
+}
+
+
+// PAGES RULE
+
+static bc_slist_t*
+pages_outputlist(bm_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->settings->pages == NULL)
+ return NULL;
+
+ const char *output_dir = bc_trie_lookup(ctx->settings->settings,
+ "output_dir");
+ const char *html_ext = bc_trie_lookup(ctx->settings->settings, "html_ext");
+
+ bc_slist_t *rv = NULL;
+ for (size_t i = 0; ctx->settings->pages[i] != NULL; i++) {
+ bool is_index = (0 == strcmp(ctx->settings->pages[i], "index"))
+ && (html_ext[0] == '/');
+ char *f = bc_strdup_printf("%s%s%s%s", output_dir,
+ is_index ? "" : "/", is_index ? "" : ctx->settings->pages[i],
+ html_ext);
+ rv = bc_slist_append(rv, bm_filectx_new(ctx, f));
+ free(f);
+ }
+ return rv;
+}
+
+static int
+pages_exec(bm_ctx_t *ctx, bc_slist_t *outputs, bool verbose)
+{
+ if (ctx == NULL || ctx->settings->pages == NULL)
+ return 0;
+
+ int rv = 0;
+
+ bc_trie_t *variables = bc_trie_new(free);
+ bc_trie_insert(variables, "DATE_FORMAT",
+ bc_strdup(bc_trie_lookup(ctx->settings->settings, "date_format")));
+ bc_trie_insert(variables, "BM_RULE", bc_strdup("pages"));
+ bc_trie_insert(variables, "BM_TYPE", bc_strdup("page"));
+
+ bc_slist_t *s, *o;
+
+ for (s = ctx->pages_fctx, o = outputs; s != NULL && o != NULL;
+ s = s->next, o = o->next)
+ {
+ bm_filectx_t *o_fctx = o->data;
+ if (o_fctx == NULL)
+ continue;
+ if (bm_rule_need_rebuild(s, ctx->settings_fctx,
+ ctx->main_template_fctx, o_fctx, true))
+ {
+ rv = bm_exec_blogc(ctx->settings, variables, false,
+ ctx->main_template_fctx, o_fctx, s, verbose, true);
+ if (rv != 0)
+ break;
+ }
+ }
+
+ bc_trie_free(variables);
+
+ return rv;
+}
+
+
+// COPY FILES RULE
+
+static bc_slist_t*
+copy_files_outputlist(bm_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->settings->copy_files == NULL)
+ return NULL;
+
+ bc_slist_t *rv = NULL;
+ const char *dir = bc_trie_lookup(ctx->settings->settings, "output_dir");
+ for (size_t i = 0; ctx->settings->copy_files[i] != NULL; i++) {
+ char *f = bc_strdup_printf("%s/%s", dir, ctx->settings->copy_files[i]);
+ rv = bc_slist_append(rv, bm_filectx_new(ctx, f));
+ free(f);
+ }
+ return rv;
+}
+
+static int
+copy_files_exec(bm_ctx_t *ctx, bc_slist_t *outputs, bool verbose)
+{
+ if (ctx == NULL || ctx->settings->copy_files == NULL)
+ return 0;
+
+ int rv = 0;
+
+ bc_slist_t *s, *o;
+
+ for (s = ctx->copy_files_fctx, o = outputs; s != NULL && o != NULL;
+ s = s->next, o = o->next)
+ {
+ bm_filectx_t *o_fctx = o->data;
+ if (o_fctx == NULL)
+ continue;
+
+ if (bm_rule_need_rebuild(s, ctx->settings_fctx, NULL, o_fctx, true)) {
+ rv = bm_exec_native_cp(s->data, o_fctx, verbose);
+ if (rv != 0)
+ break;
+ }
+ }
+
+ return rv;
+}
+
+
+// CLEAN RULE
+
+static bc_slist_t*
+clean_outputlist(bm_ctx_t *ctx)
+{
+ return bm_rule_list_built_files(ctx);
+}
+
+static int
+clean_exec(bm_ctx_t *ctx, bc_slist_t *outputs, bool verbose)
+{
+ int rv = 0;
+
+ for (bc_slist_t *l = outputs; l != NULL; l = l->next)
+ {
+ bm_filectx_t *fctx = l->data;
+ if (fctx == NULL)
+ continue;
+
+ if (fctx->readable) {
+ rv = bm_exec_native_rm(fctx, verbose);
+ if (rv != 0)
+ break;
+ }
+ }
+
+ return rv;
+}
+
+
+const bm_rule_t const rules[] = {
+ {
+ .name = "index",
+ .help = "build website index from posts",
+ .outputlist_func = index_outputlist,
+ .exec_func = index_exec,
+ .generate_files = true,
+ },
+ {
+ .name = "atom",
+ .help = "build main atom feed from posts",
+ .outputlist_func = atom_outputlist,
+ .exec_func = atom_exec,
+ .generate_files = true,
+ },
+ {
+ .name = "atom_tags",
+ .help = "build atom feeds for each tag from posts",
+ .outputlist_func = atom_tags_outputlist,
+ .exec_func = atom_tags_exec,
+ .generate_files = true,
+ },
+ {
+ .name = "pagination",
+ .help = "build pagination pages from posts",
+ .outputlist_func = pagination_outputlist,
+ .exec_func = pagination_exec,
+ .generate_files = true,
+ },
+ {
+ .name = "posts",
+ .help = "build individual pages for each post",
+ .outputlist_func = posts_outputlist,
+ .exec_func = posts_exec,
+ .generate_files = true,
+ },
+ {
+ .name = "tags",
+ .help = "build post listings for each tag from posts",
+ .outputlist_func = tags_outputlist,
+ .exec_func = tags_exec,
+ .generate_files = true,
+ },
+ {
+ .name = "pages",
+ .help = "build individual pages for each page",
+ .outputlist_func = pages_outputlist,
+ .exec_func = pages_exec,
+ .generate_files = true,
+ },
+ {
+ .name = "copy_files",
+ .help = "copy static files from source directory to output directory",
+ .outputlist_func = copy_files_outputlist,
+ .exec_func = copy_files_exec,
+ .generate_files = true,
+ },
+ {
+ .name = "clean",
+ .help = "clean built files and empty directories in output directory",
+ .outputlist_func = clean_outputlist,
+ .exec_func = clean_exec,
+ .generate_files = false,
+ },
+ {NULL, NULL, NULL, NULL, false},
+};
+
+
+int
+bm_rule_executor(bm_ctx_t *ctx, bc_slist_t *rule_list, bool verbose)
+{
+ const bm_rule_t *rule = NULL;
+ int rv = 0;
+
+ for (bc_slist_t *l = rule_list; l != NULL; l = l->next) {
+ if (0 == strcmp("all", (char*) l->data)) {
+ bc_slist_t *s = NULL;
+ for (size_t i = 0; rules[i].name != NULL; i++) {
+ if (!rules[i].generate_files) {
+ continue;
+ }
+ s = bc_slist_append(s, bc_strdup(rules[i].name));
+ }
+ rv = bm_rule_executor(ctx, s, verbose);
+ bc_slist_free_full(s, free);
+ continue;
+ }
+ rule = NULL;
+ for (size_t i = 0; rules[i].name != NULL; i++) {
+ if (0 == strcmp((char*) l->data, rules[i].name)) {
+ rule = &(rules[i]);
+ rv = bm_rule_execute(ctx, rule, verbose);
+ if (rv != 0)
+ return rv;
+ }
+ }
+ if (rule == NULL) {
+ fprintf(stderr, "blogc-make: error: rule not found: %s\n",
+ (char*) l->data);
+ rv = 3;
+ }
+ }
+
+ return rv;
+}
+
+
+int
+bm_rule_execute(bm_ctx_t *ctx, const bm_rule_t *rule, bool verbose)
+{
+ if (rule == NULL)
+ return 3;
+
+ bc_slist_t *outputs = rule->outputlist_func(ctx);
+ int rv = rule->exec_func(ctx, outputs, verbose);
+
+ bc_slist_free_full(outputs, (bc_free_func_t) bm_filectx_free);
+
+ return rv;
+}
+
+
+bool
+bm_rule_need_rebuild(bc_slist_t *sources, bm_filectx_t *settings,
+ bm_filectx_t *template, bm_filectx_t *output, bool only_first_source)
+{
+ if (output == NULL || !output->readable)
+ return true;
+
+ bool rv = false;
+
+ bc_slist_t *s = NULL;
+ if (settings != NULL)
+ s = bc_slist_append(s, settings);
+ if (template != NULL)
+ s = bc_slist_append(s, template);
+
+ for (bc_slist_t *l = sources; l != NULL; l = l->next) {
+ s = bc_slist_append(s, l->data);
+ if (only_first_source)
+ break;
+ }
+
+ for (bc_slist_t *l = s; l != NULL; l = l->next) {
+ bm_filectx_t *source = l->data;
+ if (source == NULL || !source->readable) {
+ // this is unlikely to happen, but lets just say that we need
+ // a rebuild and let blogc bail out.
+ rv = true;
+ break;
+ }
+ if (source->timestamp.tv_sec == output->timestamp.tv_sec) {
+ if (source->timestamp.tv_nsec > output->timestamp.tv_nsec) {
+ rv = true;
+ break;
+ }
+ }
+ else if (source->timestamp.tv_sec > output->timestamp.tv_sec) {
+ rv = true;
+ break;
+ }
+ }
+
+ bc_slist_free(s);
+
+ return rv;
+}
+
+
+bc_slist_t*
+bm_rule_list_built_files(bm_ctx_t *ctx)
+{
+ if (ctx == NULL)
+ return NULL;
+
+ bc_slist_t *rv = NULL;
+ for (size_t i = 0; rules[i].name != NULL; i++) {
+ if (!rules[i].generate_files) {
+ continue;
+ }
+
+ bc_slist_t *o = rules[i].outputlist_func(ctx);
+ for (bc_slist_t *l = o; l != NULL; l = l->next) {
+ rv = bc_slist_append(rv, l->data);
+ }
+ bc_slist_free(o);
+ }
+ return rv;
+}
+
+
+void
+bm_rule_print_help(void)
+{
+ printf(
+ "\n"
+ "build rules:\n"
+ " all run all rules that generate output files\n");
+
+ for (size_t i = 0; rules[i].name != NULL; i++) {
+ printf(" %-12s %s\n", rules[i].name, rules[i].help);
+ }
+}
diff --git a/src/blogc-make/rules.h b/src/blogc-make/rules.h
new file mode 100644
index 0000000..b39e7be
--- /dev/null
+++ b/src/blogc-make/rules.h
@@ -0,0 +1,35 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#ifndef _MAKE_RULES_H
+#define _MAKE_RULES_H
+
+#include <stdbool.h>
+#include "ctx.h"
+#include "../common/utils.h"
+
+typedef bc_slist_t* (*bm_rule_outputlist_func_t) (bm_ctx_t *ctx);
+typedef int (*bm_rule_exec_func_t) (bm_ctx_t *ctx, bc_slist_t *outputs,
+ bool verbose);
+
+typedef struct {
+ const char *name;
+ const char *help;
+ bm_rule_outputlist_func_t outputlist_func;
+ bm_rule_exec_func_t exec_func;
+ bool generate_files;
+} bm_rule_t;
+
+int bm_rule_executor(bm_ctx_t *ctx, bc_slist_t *rule_list, bool verbose);
+int bm_rule_execute(bm_ctx_t *ctx, const bm_rule_t *rule, bool verbose);
+bool bm_rule_need_rebuild(bc_slist_t *sources, bm_filectx_t *settings,
+ bm_filectx_t *template, bm_filectx_t *output, bool only_first_source);
+bc_slist_t* bm_rule_list_built_files(bm_ctx_t *ctx);
+void bm_rule_print_help(void);
+
+#endif /* _MAKE_RULES_H */
diff --git a/src/blogc-make/settings.c b/src/blogc-make/settings.c
new file mode 100644
index 0000000..e2789dc
--- /dev/null
+++ b/src/blogc-make/settings.c
@@ -0,0 +1,175 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#include <libgen.h>
+#include <stdlib.h>
+#include "../common/config-parser.h"
+#include "../common/error.h"
+#include "../common/file.h"
+#include "../common/utils.h"
+#include "settings.h"
+
+
+static const struct default_settings_map {
+ const char *key;
+ const char *default_value;
+} default_settings[] = {
+
+ // source
+ {"content_dir", "content"},
+ {"template_dir", "templates"},
+ {"main_template", "main.tmpl"},
+ {"source_ext", ".txt"},
+
+ // output
+ {"output_dir", "_build"},
+
+ // pagination
+ {"pagination_prefix", "page"},
+ {"posts_per_page", "10"},
+ {"atom_posts_per_page", "10"},
+
+ // html
+ {"html_ext", "/index.html"},
+ {"index_prefix", NULL},
+ {"post_prefix", "post"},
+ {"tag_prefix", "tag"},
+
+ // atom
+ {"atom_prefix", "atom"},
+ {"atom_ext", ".xml"},
+
+ // generic
+ {"date_format", "%b %d, %Y, %I:%M %p GMT"},
+ {"locale", NULL},
+
+ {NULL, NULL},
+};
+
+
+static const char* required_environment[] = {
+ "AUTHOR_NAME",
+ "AUTHOR_EMAIL",
+ "SITE_TITLE",
+ "SITE_TAGLINE",
+ "BASE_DOMAIN",
+ NULL,
+};
+
+
+static const char* list_sections[] = {
+ "posts",
+ "pages",
+ "copy_files",
+ "tags",
+ NULL,
+};
+
+
+bm_settings_t*
+bm_settings_parse(const char *content, size_t content_len, bc_error_t **err)
+{
+ if (err == NULL || *err != NULL)
+ return NULL;
+
+ if (content == NULL)
+ return NULL;
+
+ bc_config_t *config = bc_config_parse(content, content_len, list_sections,
+ err);
+ if (config == NULL || (err != NULL && *err != NULL))
+ return NULL;
+
+ bm_settings_t *rv = bc_malloc(sizeof(bm_settings_t));
+ rv->root_dir = NULL;
+ rv->env = bc_trie_new(free);
+ rv->settings = bc_trie_new(free);
+ rv->posts = NULL;
+ rv->pages = NULL;
+ rv->copy_files = NULL;
+ rv->tags = NULL;
+
+ char **env = bc_config_list_keys(config, "environment");
+ if (env != NULL) {
+ for (size_t i = 0; env[i] != NULL; i++) {
+ // FIXME: validate keys
+ bc_trie_insert(rv->env, env[i],
+ bc_strdup(bc_config_get(config, "environment", env[i])));
+ }
+ }
+ bc_strv_free(env);
+
+ for (size_t i = 0; required_environment[i] != NULL; i++) {
+ const char *value = bc_trie_lookup(rv->env, required_environment[i]);
+ if (value == NULL || value[0] == '\0') {
+ *err = bc_error_new_printf(BLOGC_MAKE_ERROR_SETTINGS,
+ "[environment] key required but not found or empty: %s",
+ required_environment[i]);
+ bm_settings_free(rv);
+ rv = NULL;
+ goto cleanup;
+ }
+ }
+
+ for (size_t i = 0; default_settings[i].key != NULL; i++) {
+ const char *value = bc_config_get_with_default(
+ config, "settings", default_settings[i].key,
+ default_settings[i].default_value);
+ if (value != NULL) {
+ bc_trie_insert(rv->settings, default_settings[i].key,
+ bc_strdup(value));
+ }
+ }
+
+ rv->posts = bc_config_get_list(config, "posts");
+ rv->pages = bc_config_get_list(config, "pages");
+ rv->copy_files = bc_config_get_list(config, "copy_files");
+ rv->tags = bc_config_get_list(config, "tags");
+
+cleanup:
+
+ bc_config_free(config);
+
+ return rv;
+}
+
+
+bm_settings_t*
+bm_settings_parse_file(const char *filename, bc_error_t **err)
+{
+ if (err == NULL || *err != NULL)
+ return NULL;
+
+ size_t content_len;
+ char *content = bc_file_get_contents(filename, true, &content_len, err);
+ if (*err != NULL)
+ return NULL;
+
+ bm_settings_t *rv = bm_settings_parse(content, content_len, err);
+ char *real_filename = realpath(filename, NULL);
+ rv->root_dir = bc_strdup(dirname(real_filename));
+ free(real_filename);
+ free(content);
+ return rv;
+}
+
+
+void
+bm_settings_free(bm_settings_t *settings)
+{
+ if (settings == NULL)
+ return;
+ free(settings->root_dir);
+ bc_trie_free(settings->env);
+ bc_trie_free(settings->settings);
+ bc_strv_free(settings->posts);
+ bc_strv_free(settings->pages);
+ bc_strv_free(settings->copy_files);
+ bc_strv_free(settings->tags);
+ free(settings);
+}
diff --git a/src/blogc-make/settings.h b/src/blogc-make/settings.h
new file mode 100644
index 0000000..0ba9545
--- /dev/null
+++ b/src/blogc-make/settings.h
@@ -0,0 +1,30 @@
+/*
+ * blogc: A blog compiler.
+ * Copyright (C) 2016 Rafael G. Martins <rafael@rafaelmartins.eng.br>
+ *
+ * This program can be distributed under the terms of the BSD License.
+ * See the file LICENSE.
+ */
+
+#ifndef _MAKE_SETTINGS_H
+#define _MAKE_SETTINGS_H
+
+#include "../common/error.h"
+#include "../common/utils.h"
+
+typedef struct {
+ char *root_dir;
+ bc_trie_t *env;
+ bc_trie_t *settings;
+ char **posts;
+ char **pages;
+ char **copy_files;
+ char **tags;
+} bm_settings_t;
+
+bm_settings_t* bm_settings_parse(const char *content, size_t content_len,
+ bc_error_t **err);
+bm_settings_t* bm_settings_parse_file(const char *filename, bc_error_t **err);
+void bm_settings_free(bm_settings_t *settings);
+
+#endif /* _MAKE_SETTINGS_H */