Index: uspace/app/download/main.c
===================================================================
--- uspace/app/download/main.c	(revision b93ea46a57ff6d614900fa458bc55289fa9dbf47)
+++ uspace/app/download/main.c	(revision 8ebc5b8afff1eee31b5dccfd149defa4473bba75)
@@ -57,6 +57,6 @@
 static void syntax_print(void)
 {
-	fprintf(stderr, "Usage: download <url>\n");
-	fprintf(stderr, "  This will write the data to stdout, so you may want\n");
+	fprintf(stderr, "Usage: download [-o <outfile>] <url>\n");
+	fprintf(stderr, "  Without -o, data will be written to stdout, so you may want\n");
 	fprintf(stderr, "  to redirect the output, e.g.\n");
 	fprintf(stderr, "\n");
@@ -66,18 +66,54 @@
 int main(int argc, char *argv[])
 {
-	if (argc != 2) {
+	int i;
+	char *ofname = NULL;
+	FILE *ofile = NULL;
+	size_t buf_size = 4096;
+	void *buf = NULL;
+	uri_t *uri = NULL;
+	int rc;
+
+	if (argc < 2) {
 		syntax_print();
-		return 2;
-	}
-	
-	uri_t *uri = uri_parse(argv[1]);
+		rc = EINVAL;
+		goto error;
+	}
+	
+	i = 1;
+	
+	if (str_cmp(argv[i], "-o") == 0) {
+		++i;
+		if (argc < i + 1) {
+			syntax_print();
+			rc = EINVAL;
+			goto error;
+		}
+		
+		ofname = argv[i++];
+		ofile = fopen(ofname, "wb");
+		if (ofile == NULL) {
+			fprintf(stderr, "Error creating '%s'.\n", ofname);
+			rc = EINVAL;
+			goto error;
+		}
+	}
+	
+	if (argc != i + 1) {
+		syntax_print();
+		rc = EINVAL;
+		goto error;
+	}
+	
+	uri = uri_parse(argv[i]);
 	if (uri == NULL) {
 		fprintf(stderr, "Failed parsing URI\n");
-		return 2;
+		rc = EINVAL;
+		goto error;
 	}
 	
 	if (!uri_validate(uri)) {
 		fprintf(stderr, "The URI is invalid\n");
-		return 2;
+		rc = EINVAL;
+		goto error;
 	}
 	
@@ -86,18 +122,21 @@
 	if (str_cmp(uri->scheme, "http") != 0) {
 		fprintf(stderr, "Only http scheme is supported at the moment\n");
-		return 2;
+		rc = EINVAL;
+		goto error;
 	}
 	
 	if (uri->host == NULL) {
 		fprintf(stderr, "host not set\n");
-		return 2;
+		rc = EINVAL;
+		goto error;
 	}
 	
 	uint16_t port = 80;
 	if (uri->port != NULL) {
-		int rc = str_uint16_t(uri->port, NULL, 10, true, &port);
+		rc = str_uint16_t(uri->port, NULL, 10, true, &port);
 		if (rc != EOK) {
 			fprintf(stderr, "Invalid port number: %s\n", uri->port);
-			return 2;
+			rc = EINVAL;
+			goto error;
 		}
 	}
@@ -111,14 +150,13 @@
 		if (server_path == NULL) {
 			fprintf(stderr, "Failed allocating path\n");
-			uri_destroy(uri);
-			return 3;
-		}
-	}
-	else {
-		int rc = asprintf(&server_path, "%s?%s", path, uri->query);
+			rc = ENOMEM;
+			goto error;
+		}
+	} else {
+		rc = asprintf(&server_path, "%s?%s", path, uri->query);
 		if (rc < 0) {
 			fprintf(stderr, "Failed allocating path\n");
-			uri_destroy(uri);
-			return 3;
+			rc = ENOMEM;
+			goto error;
 		}
 	}
@@ -128,13 +166,12 @@
 	if (req == NULL) {
 		fprintf(stderr, "Failed creating request\n");
-		uri_destroy(uri);
-		return 3;
-	}
-	
-	int rc = http_headers_append(&req->headers, "Host", uri->host);
+		rc = ENOMEM;
+		goto error;
+	}
+	
+	rc = http_headers_append(&req->headers, "Host", uri->host);
 	if (rc != EOK) {
 		fprintf(stderr, "Failed setting Host header: %s\n", str_error(rc));
-		uri_destroy(uri);
-		return rc;
+		goto error;
 	}
 	
@@ -142,13 +179,12 @@
 	if (rc != EOK) {
 		fprintf(stderr, "Failed creating User-Agent header: %s\n", str_error(rc));
-		uri_destroy(uri);
-		return rc;
+		goto error;
 	}
 	
 	http_t *http = http_create(uri->host, port);
 	if (http == NULL) {
-		uri_destroy(uri);
 		fprintf(stderr, "Failed creating HTTP object\n");
-		return 3;
+		rc = ENOMEM;
+		goto error;
 	}
 	
@@ -156,6 +192,6 @@
 	if (rc != EOK) {
 		fprintf(stderr, "Failed connecting: %s\n", str_error(rc));
-		uri_destroy(uri);
-		return rc;
+		rc = EIO;
+		goto error;
 	}
 	
@@ -163,6 +199,6 @@
 	if (rc != EOK) {
 		fprintf(stderr, "Failed sending request: %s\n", str_error(rc));
-		uri_destroy(uri);
-		return rc;
+		rc = EIO;
+		goto error;
 	}
 	
@@ -172,6 +208,6 @@
 	if (rc != EOK) {
 		fprintf(stderr, "Failed receiving response: %s\n", str_error(rc));
-		uri_destroy(uri);
-		return rc;
+		rc = EIO;
+		goto error;
 	}
 	
@@ -179,17 +215,15 @@
 		fprintf(stderr, "Server returned status %d %s\n", response->status,
 		    response->message);
-	}
-	else {
-		size_t buf_size = 4096;
-		void *buf = malloc(buf_size);
+	} else {
+		buf = malloc(buf_size);
 		if (buf == NULL) {
 			fprintf(stderr, "Failed allocating buffer\n)");
-			uri_destroy(uri);
-			return ENOMEM;
+			rc = ENOMEM;
+			goto error;
 		}
 		
 		int body_size;
 		while ((body_size = recv_buffer(&http->recv_buffer, buf, buf_size)) > 0) {
-			fwrite(buf, 1, body_size, stdout);
+			fwrite(buf, 1, body_size, ofile != NULL ? ofile : stdout);
 		}
 		
@@ -199,6 +233,19 @@
 	}
 	
+	free(buf);
 	uri_destroy(uri);
+	if (fclose(ofile) != 0) {
+		printf("Error writing '%s'.\n", ofname);
+		return EIO;
+	}
+
 	return EOK;
+error:
+	free(buf);
+	if (uri != NULL)
+		uri_destroy(uri);
+	if (ofile != NULL)
+		fclose(ofile);
+	return rc;
 }
 
Index: uspace/app/pkg/Makefile
===================================================================
--- uspace/app/pkg/Makefile	(revision 8ebc5b8afff1eee31b5dccfd149defa4473bba75)
+++ uspace/app/pkg/Makefile	(revision 8ebc5b8afff1eee31b5dccfd149defa4473bba75)
@@ -0,0 +1,35 @@
+#
+# Copyright (c) 2017 Jiri Svoboda
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# - Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+# - 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.
+# - The name of the author may not be used to endorse or promote products
+#   derived from this software without specific prior written permission.
+#
+# 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.
+#
+
+USPACE_PREFIX = ../..
+BINARY = pkg
+
+SOURCES = \
+	pkg.c
+
+include $(USPACE_PREFIX)/Makefile.common
Index: uspace/app/pkg/pkg.c
===================================================================
--- uspace/app/pkg/pkg.c	(revision 8ebc5b8afff1eee31b5dccfd149defa4473bba75)
+++ uspace/app/pkg/pkg.c	(revision 8ebc5b8afff1eee31b5dccfd149defa4473bba75)
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2017 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - 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.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * 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.
+ */
+
+/** @addtogroup pkg
+ * @{
+ */
+/** @file Package installer
+ *
+ * Utility to simplify installation of coastline packages
+ */
+
+#include <errno.h>
+#include <macros.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <str_error.h>
+#include <task.h>
+
+#define NAME "pkg"
+
+static void print_syntax(void)
+{
+	fprintf(stderr, "syntax: " NAME " install <package-name>\n");
+}
+
+static int cmd_runl(const char *path, ...)
+{
+	va_list ap;
+	const char *arg;
+	int cnt = 0;
+
+	va_start(ap, path);
+	do {
+		arg = va_arg(ap, const char *);
+		cnt++;
+	} while (arg != NULL);
+	va_end(ap);
+
+	va_start(ap, path);
+	task_id_t id;
+	task_wait_t wait;
+	int rc = task_spawn(&id, &wait, path, cnt, ap);
+	va_end(ap);
+
+	if (rc != EOK) {
+		printf("Error spawning %s (%s)\n", path, str_error(rc));
+		return rc;
+	}
+
+	if (!id) {
+		printf("Error spawning %s (invalid task ID)\n", path);
+		return EINVAL;
+	}
+
+	task_exit_t texit;
+	int retval;
+	rc = task_wait(&wait, &texit, &retval);
+	if (rc != EOK) {
+		printf("Error waiting for %s (%s)\n", path, str_error(rc));
+		return rc;
+	}
+
+	if (texit != TASK_EXIT_NORMAL) {
+		printf("Command %s unexpectedly terminated\n", path);
+		return EINVAL;
+	}
+
+	if (retval != 0) {
+		printf("Command %s returned non-zero exit code %d)\n",
+		    path, retval);
+	}
+
+	return retval;
+}
+
+
+static int pkg_install(int argc, char *argv[])
+{
+	char *pkg_name;
+	char *src_uri;
+	char *fname;
+	char *fnunpack;
+	int rc;
+
+	if (argc != 3) {
+		print_syntax();
+		return EINVAL;
+	}
+
+	pkg_name = argv[2];
+
+	rc = asprintf(&src_uri, "http://ci.helenos.org/latest/" STRING(UARCH)
+	    "/%s-for-helenos-" STRING(UARCH) ".tar.gz",
+	    pkg_name);
+	if (rc < 0) {
+		printf("Out of memory.\n");
+		return ENOMEM;
+	}
+
+	rc = asprintf(&fname, "/tmp/%s-for-helenos-" STRING(UARCH)
+	    ".tar.gz", pkg_name);
+	if (rc < 0) {
+		printf("Out of memory.\n");
+		return ENOMEM;
+	}
+
+	rc = asprintf(&fnunpack, "/tmp/%s-for-helenos-" STRING(UARCH) ".tar",
+	    pkg_name);
+	if (rc < 0) {
+		printf("Out of memory.\n");
+		return ENOMEM;
+	}
+	/*XXX error cleanup */
+
+	printf("Downloading '%s'.\n", src_uri);
+
+	rc = cmd_runl("/app/download", "/app/download", "-o", fname,
+	    src_uri, NULL);
+	if (rc != EOK) {
+		printf("Error downloading package archive.\n");
+		return rc;
+	}
+
+	printf("Extracting package\n");
+
+	rc = cmd_runl("/app/gunzip", "/app/gunzip", fname, fnunpack, NULL);
+	if (rc != EOK) {
+		printf("Error uncompressing package archive.\n");
+		return rc;
+	}
+
+	if (remove(fname) != 0) {
+		printf("Error deleting package archive.\n");
+		return rc;
+	}
+
+
+	rc = cmd_runl("/app/untar", "/app/untar", fnunpack, NULL);
+	if (rc != EOK) {
+		printf("Error extracting package archive.\n");
+		return rc;
+	}
+
+	if (remove(fnunpack) != 0) {
+		printf("Error deleting package archive.\n");
+		return rc;
+	}
+
+	printf("Package '%s' installed.\n", pkg_name);
+
+	return EOK;
+}
+
+int main(int argc, char *argv[])
+{
+	char *cmd;
+	int rc;
+
+	if (argc < 2) {
+		fprintf(stderr, "Arguments missing.\n");
+		print_syntax();
+		return 1;
+	}
+
+	cmd = argv[1];
+
+	if (str_cmp(cmd, "install") == 0) {
+		rc = pkg_install(argc, argv);
+	} else {
+		fprintf(stderr, "Unknown command.\n");
+		print_syntax();
+		return 1;
+	}
+
+	if (rc != EOK)
+		return 1;
+
+	return 0;
+}
+
+/** @}
+ */
