diff --git a/bin/Makefile b/bin/Makefile index bd5e71d9..e012a577 100644 --- a/bin/Makefile +++ b/bin/Makefile @@ -4,5 +4,6 @@ SUBDIRS = xbps-bin SUBDIRS += xbps-repo SUBDIRS += xbps-dgraph SUBDIRS += xbps-uhelper +SUBDIRS += xbps-create include ../mk/subdir.mk diff --git a/bin/xbps-create/Makefile b/bin/xbps-create/Makefile new file mode 100644 index 00000000..47c393c5 --- /dev/null +++ b/bin/xbps-create/Makefile @@ -0,0 +1,7 @@ +TOPDIR = ../.. +-include $(TOPDIR)/config.mk + +BIN = xbps-create +OBJS = main.o + +include $(TOPDIR)/mk/prog.mk diff --git a/bin/xbps-create/main.c b/bin/xbps-create/main.c new file mode 100644 index 00000000..8729bfde --- /dev/null +++ b/bin/xbps-create/main.c @@ -0,0 +1,581 @@ +/*- + * Copyright (c) 2012 Juan Romero Pardines. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "queue.h" + +#define _PROGNAME "xbps-create" + +struct xentry { + TAILQ_ENTRY(xentry) entries; + char *file, *type, *target, *hash; +}; + +static TAILQ_HEAD(xentry_head, xentry) xentry_list = + TAILQ_HEAD_INITIALIZER(xentry_list); + +static prop_dictionary_t pkg_propsd, pkg_filesd; +static const char *destdir; + +static void __attribute__((noreturn)) +usage(void) +{ + fprintf(stderr, + "usage: %s [options] [file1] [file2] ...\n\n" + " Options:\n" + " -A, --architecture Package architecture (e.g: noarch, i686, etc).\n" + " -C, --conflicts Conflicts (blank separated list,\n" + " e.g: 'foo>=2.0 blah<=2.0').\n" + " -D, --dependencies Dependencies (blank separated list,\n" + " e.g: 'foo>=1.0_1 blah<2.1').\n" + " -d, --destdir Package destdir.\n" + " -F, --config-files Configuration files (blank separated list,\n" + " e.g '/etc/foo.conf /etc/foo-blah.conf').\n" + " -h, --homepage Homepage.\n" + " -l, --license License.\n" + " -M, --mutable-files Mutable files list (blank separated list,\n" + " e.g: '/usr/lib/foo /usr/bin/blah').\n" + " -m, --maintainer Maintainer.\n" + " -n, --pkgver Package name/version tuple (e.g `foo-1.0_1').\n" + " -P, --provides Provides (blank separated list,\n" + " e.g: 'foo-9999 blah-1.0').\n" + " -p, --preserve Enable package preserve boolean.\n" + " -R, --replaces Replaces (blank separated list,\n" + " e.g: 'foo>=1.0 blah<2.0').\n" + " -S, --long-desc Long description (80 cols per line).\n" + " -s, --desc Short description (max 80 characters).\n" + " -V, --version Prints the xbps release version\n\n" + " Example:\n" + " $ %s -A noarch -n foo-1.0_1 -s \"foo pkg\" -d dir\n", + _PROGNAME, _PROGNAME); + exit(EXIT_FAILURE); +} + +static void __attribute__((noreturn)) +die(const char *fmt, ...) +{ + va_list ap; + int save_errno = errno; + + va_start(ap, fmt); + fprintf(stderr, "%s: ERROR: ", _PROGNAME); + vfprintf(stderr, fmt, ap); + fprintf(stderr, " %s\n", save_errno ? strerror(save_errno) : ""); + va_end(ap); + exit(EXIT_FAILURE); +} + +static void +process_array(const char *key, const char *val) +{ + prop_array_t array; + char *args, *p = NULL, *saveptr; + + assert(key); + + if (val == NULL) + return; + + array = prop_array_create(); + assert(array); + + if (strchr(val, ' ') == NULL) { + prop_array_add_cstring_nocopy(array, val); + goto out; + } + + args = strdup(val); + assert(args); + + for ((p = strtok_r(args, " ", &saveptr)); p; + (p = strtok_r(NULL, " ", &saveptr))) { + if (p == NULL) + continue; + prop_array_add_cstring(array, p); + } + free(args); +out: + prop_dictionary_set(pkg_propsd, key, array); + prop_object_release(array); +} + +static bool +entry_is_conf_file(const char *file) +{ + prop_array_t a; + const char *curfile; + size_t i; + + assert(file); + + a = prop_dictionary_get(pkg_propsd, "conf_files"); + if (a == NULL || prop_array_count(a) == 0) + return false; + + for (i = 0; i < prop_array_count(a); i++) { + prop_array_get_cstring_nocopy(a, i, &curfile); + if (strcmp(file, curfile) == 0) + return true; + } + return false; +} + +static int +ftw_cb(const char *fpath, const struct stat *sb, int type, struct FTW *ftwbuf) +{ + struct xentry *xe = NULL; + const char *filep = NULL; + char buf[PATH_MAX]; + + (void)sb; + (void)ftwbuf; + + if ((strcmp(fpath, ".") == 0) || + (strcmp(fpath, "./props.plist") == 0) || + (strcmp(fpath, "./files.plist") == 0)) + return 0; + + /* sanitized file path */ + filep = strchr(fpath, '/') + 1; + + xe = calloc(1, sizeof(*xe)); + assert(xe); + xe->file = strdup(filep); + assert(xe->file); + + if ((strcmp(fpath, "./INSTALL") == 0) || + (strcmp(fpath, "./REMOVE") == 0)) { + /* metadata file */ + xe->type = strdup("metadata"); + assert(xe->type); + goto out; + } + + if (type == FTW_SL) { + /* symlink */ + xe->type = strdup("links"); + assert(xe->type); + /* Find out target file */ + memset(&buf, 0, sizeof(buf)); + if (realpath(fpath, buf) == NULL) + die("failed to process symlink `%s':", filep); + + filep = buf + strlen(destdir); + xe->target = strdup(filep); + assert(xe->target); + } else if (type == FTW_F) { + /* file */ + /* + * Find out if it's a configuration file or not. + */ + if (entry_is_conf_file(filep)) + xe->type = strdup("conf_files"); + else + xe->type = strdup("files"); + + assert(xe->type); + /* calculate sha256 hash */ + if ((xe->hash = xbps_file_hash(fpath)) == NULL) + die("failed to process hash for %s:", fpath); + } else if (type == FTW_D) { + /* directory */ + xe->type = strdup("dirs"); + assert(xe->type); + } + +out: + TAILQ_INSERT_TAIL(&xentry_list, xe, entries); + return 0; +} + +static void +process_xentry(const char *key) +{ + prop_array_t a; + prop_dictionary_t d; + struct xentry *xe; + bool found = false; + + a = prop_array_create(); + assert(a); + + TAILQ_FOREACH_REVERSE(xe, &xentry_list, xentry_head, entries) { + if (strcmp(xe->type, key)) + continue; + + found = true; + d = prop_dictionary_create(); + assert(d); + prop_dictionary_set_cstring(d, "file", xe->file); + if (xe->target) + prop_dictionary_set_cstring(d, "target", xe->target); + else if (xe->hash) + prop_dictionary_set_cstring(d, "sha256", xe->hash); + prop_array_add(a, d); + prop_object_release(d); + } + if (found) + prop_dictionary_set(pkg_filesd, key, a); + + prop_object_release(a); +} + +static void +process_destdir(void) +{ + if (nftw(".", ftw_cb, 20, FTW_PHYS|FTW_MOUNT) != 0) + die("failed to process destdir files (nftw):"); + + /* Process regular files */ + process_xentry("files"); + + /* Process configuration files */ + process_xentry("conf_files"); + + /* Process symlinks */ + process_xentry("links"); + + /* Process directories */ + process_xentry("dirs"); +} + +static void +process_entry_file(struct archive *ar, struct xentry *xe, const char *filematch) +{ + struct archive *ard; + struct archive_entry *entry; + char buf[BUFSIZ]; + int fd; + size_t len; + + assert(ar); + assert(xe); + + if (filematch && strcmp(xe->file, filematch)) + return; + + ard = archive_read_disk_new(); + assert(ard); + archive_read_disk_set_standard_lookup(ard); + archive_read_disk_set_symlink_physical(ard); + + entry = archive_entry_new(); + assert(entry); + archive_entry_set_pathname(entry, xe->file); + + if ((fd = open(xe->file, O_RDONLY)) == -1) + die("failed to add entry (open) %s to archive:", xe->file); + + if ((archive_read_disk_entry_from_file(ard, entry, fd, NULL)) != 0) + die("failed to add entry %s to archive:", xe->file); + + archive_write_header(ar, entry); + + while ((len = read(fd, buf, sizeof(buf))) > 0) + archive_write_data(ar, buf, len); + + close(fd); + archive_entry_free(entry); + archive_read_free(ard); +} + +static void +process_entry_memory(struct archive *ar, const void *src, const char *file) +{ + struct archive_entry *entry; + time_t tm; + + assert(ar); + assert(src); + assert(file); + + tm = time(NULL); + + entry = archive_entry_new(); + assert(entry); + archive_entry_set_filetype(entry, AE_IFREG); + archive_entry_set_perm(entry, 0644); + archive_entry_set_uname(entry, "root"); + archive_entry_set_gname(entry, "root"); + archive_entry_set_pathname(entry, file); + archive_entry_set_size(entry, strlen(src)); + archive_entry_set_atime(entry, tm, 0); + archive_entry_set_mtime(entry, tm, 0); + archive_entry_set_ctime(entry, tm, 0); + archive_write_header(ar, entry); + archive_write_data(ar, src, strlen(src)); + archive_entry_free(entry); +} + +static void +destroy_xentry(struct xentry *xe) +{ + assert(xe); + + free(xe->file); + free(xe->type); + if (xe->target) + free(xe->target); + if (xe->hash) + free(xe->hash); + free(xe); +} + +static void +process_archive(struct archive *ar, const char *pkgver) +{ + struct xentry *xe; + char *xml; + + /* Add INSTALL/REMOVE metadata scripts first */ + TAILQ_FOREACH(xe, &xentry_list, entries) { + process_entry_file(ar, xe, "INSTALL"); + process_entry_file(ar, xe, "REMOVE"); + } + + /* Add props.plist metadata file */ + xml = prop_dictionary_externalize(pkg_propsd); + assert(xml); + process_entry_memory(ar, xml, "props.plist"); + free(xml); + + /* Add files.plist metadata file */ + xml = prop_dictionary_externalize(pkg_filesd); + assert(xml); + process_entry_memory(ar, xml, "files.plist"); + free(xml); + + /* Add all package data files and release resources */ + while ((xe = TAILQ_FIRST(&xentry_list)) != NULL) { + TAILQ_REMOVE(&xentry_list, xe, entries); + if ((strcmp(xe->type, "metadata") == 0) || + (strcmp(xe->type, "dirs") == 0)) { + destroy_xentry(xe); + continue; + } + printf("%s: adding `%s' ...\n", pkgver, xe->file); + fflush(stdout); + process_entry_file(ar, xe, NULL); + destroy_xentry(xe); + } +} + +int +main(int argc, char **argv) +{ + struct option longopts[] = { + { "architecture", required_argument, NULL, 'A' }, + { "conflicts", required_argument, NULL, 'C' }, + { "dependencies", required_argument, NULL, 'D' }, + { "destdir", required_argument, NULL, 'd' }, + { "config-files", required_argument, NULL, 'F' }, + { "homepage", required_argument, NULL, 'h' }, + { "license", required_argument, NULL, 'l' }, + { "mutable-files", required_argument, NULL, 'M' }, + { "maintainer", required_argument, NULL, 'm' }, + { "pkgver", required_argument, NULL, 'n' }, + { "provides", required_argument, NULL, 'P' }, + { "preserve", no_argument, NULL, 'p' }, + { "replaces", required_argument, NULL, 'R' }, + { "long-desc", required_argument, NULL, 'S' }, + { "desc", required_argument, NULL, 's' }, + { "version", no_argument, NULL, 'V' }, + { 0, 0, 0, 0 } + }; + struct archive *ar; + struct stat st; + const char *conflicts, *deps, *homepage, *license, *maint; + const char *provides, *pkgver, *replaces, *desc, *ldesc; + const char *arch, *config_files, *mutable_files, *version; + char *pkgname, *binpkg; + bool preserve = false; + int c; + + arch = conflicts = deps = homepage = license = maint = NULL; + provides = pkgver = replaces = desc = ldesc = NULL; + config_files = mutable_files = NULL; + + while ((c = getopt_long(argc, argv, + "A:C:D:d:F:h:l:M:m:n:P:pR:S:s:V", longopts, &c)) != -1) { + switch (c) { + case 'A': + arch = optarg; + break; + case 'C': + conflicts = optarg; + break; + case 'D': + deps = optarg; + break; + case 'd': + destdir = optarg; + break; + case 'F': + config_files = optarg; + break; + case 'h': + homepage = optarg; + break; + case 'l': + license = optarg; + break; + case 'M': + mutable_files = optarg; + break; + case 'm': + maint = optarg; + break; + case 'n': + pkgver = optarg; + break; + case 'P': + provides = optarg; + break; + case 'p': + preserve = true; + break; + case 'R': + replaces = optarg; + break; + case 'S': + ldesc = optarg; + break; + case 's': + desc = optarg; + break; + case 'V': + printf("%s\n", XBPS_RELVER); + exit(EXIT_SUCCESS); + case '?': + default: + usage(); + } + } + argc -= optind; + argv += optind; + + if (destdir == NULL) + die("destdir not set!"); + else if (pkgver == NULL) + die("pkgver not set!"); + else if (desc == NULL) + die("short description not set!"); + else if (arch == NULL) + die("architecture not set!"); + /* + * Sanity check for required options. + */ + pkgname = xbps_pkg_name(pkgver); + if (pkgname == NULL) + die("invalid pkgver! got `%s' expected `foo-1.0_1'", pkgver); + version = xbps_pkg_version(pkgver); + if (version == NULL) + die("invalid pkgver! got `%s' expected `foo-1.0_1'", pkgver); + + if (stat(destdir, &st) == -1) + die("failed to stat() destdir `%s':", destdir); + if (!S_ISDIR(st.st_mode)) + die("destdir `%s' is not a directory!", destdir); + /* + * Process XBPS_PKGPROPS metadata file. + */ + pkg_propsd = prop_dictionary_create(); + assert(pkg_propsd); + + /* Required properties */ + prop_dictionary_set_cstring_nocopy(pkg_propsd, "architecture", arch); + prop_dictionary_set_cstring_nocopy(pkg_propsd, "pkgname", pkgname); + prop_dictionary_set_cstring_nocopy(pkg_propsd, "version", version); + prop_dictionary_set_cstring_nocopy(pkg_propsd, "pkgver", pkgver); + prop_dictionary_set_cstring_nocopy(pkg_propsd, "short_desc", desc); + + /* Optional properties */ + if (homepage) + prop_dictionary_set_cstring_nocopy(pkg_propsd, "homepage", homepage); + if (license) + prop_dictionary_set_cstring_nocopy(pkg_propsd, "license", license); + if (maint) + prop_dictionary_set_cstring_nocopy(pkg_propsd, "maintainer", maint); + if (ldesc) + prop_dictionary_set_cstring_nocopy(pkg_propsd, "long_desc", ldesc); + if (preserve) + prop_dictionary_set_bool(pkg_propsd, "preserve", true); + + /* Optional arrays */ + process_array("run_depends", deps); + process_array("conf_files", config_files); + process_array("conflicts", conflicts); + process_array("provides", provides); + process_array("replaces", replaces); + + if (chdir(destdir) == -1) + die("cannot chdir() to destdir:"); + /* + * Process XBPS_PKGFILES metadata file. + */ + pkg_filesd = prop_dictionary_create(); + assert(pkg_filesd); + process_destdir(); + + /* + * Create the real XBPS binary package (ustar compressed with xz). + */ + ar = archive_write_new(); + assert(ar); + archive_write_add_filter_xz(ar); + archive_write_set_format_ustar(ar); + archive_write_set_options(ar, "compression-level=9"); + + binpkg = xbps_xasprintf("%s.%s.xbps", pkgver, arch); + assert(binpkg); + if (archive_write_open_filename(ar, binpkg) != 0) + die("failed to create %s:", binpkg); + + process_archive(ar, pkgver); + archive_write_free(ar); + + printf("%s: binary package created successfully: %s.\n", pkgver, binpkg); + + prop_object_release(pkg_propsd); + prop_object_release(pkg_filesd); + free(binpkg); + free(pkgname); + + exit(EXIT_SUCCESS); +}