diff --git a/NEWS b/NEWS
index 279715ee..a31a83a3 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,7 @@ TBD
* Prevent writing duplicate groups to metadata file
* Disable all filesystem access in flatpak-builder --run sandbox
* Add ability to set custom fusermount path
+* Support installing files from file sources
Changes in 1.4.6
================
diff --git a/data/flatpak-manifest.schema.json b/data/flatpak-manifest.schema.json
index b9bb8be4..b1e6237e 100644
--- a/data/flatpak-manifest.schema.json
+++ b/data/flatpak-manifest.schema.json
@@ -1019,6 +1019,15 @@
"dest-filename": {
"description": "Filename to for the downloaded file, defaults to the basename of url.",
"type": "string"
+ },
+ "install-dir": {
+ "description": "Directory relative to FLATPAK_DEST where the file will be installed. The directory is created if absent and the install is skipped with a warning if the file exists in the target path.",
+ "type": "string"
+ },
+ "install-mode": {
+ "description": "Optional octal permissions applied to the installed file, e.g. '755' or '0755'. Requires install-dir. If absent, the original permissions of the file are preserved.",
+ "type": "string",
+ "pattern": "^[0-7]{3,4}$"
}
},
"patternProperties": {
@@ -1037,7 +1046,8 @@
{ "required": [ "sha1" ] },
{ "required": [ "md5" ] }
]
- }
+ },
+ "install-mode": { "required": [ "install-dir" ] }
},
"additionalProperties": false
},
diff --git a/doc/flatpak-manifest.xml b/doc/flatpak-manifest.xml
index a498d10e..eb2fe5d0 100644
--- a/doc/flatpak-manifest.xml
+++ b/doc/flatpak-manifest.xml
@@ -729,6 +729,28 @@
(string)
Filename to for the downloaded file, defaults to the basename of url.
+
+ (string)
+
+
+ Install the file into this directory relative to FLATPAK_DEST.
+ The directory will be created if it doesn't exist and the install will be
+ skipped with a warning if the file already exists in the target path.
+
+
+
+
+ (string)
+
+
+ Optional octal permissions to apply to the installed file,
+ such as 0755 or 755.
+ This option requires to be set. If
+ this is not set the original permissions on the file will be
+ preserved.
+
+
+
diff --git a/src/builder-module.c b/src/builder-module.c
index ea10095a..13615617 100644
--- a/src/builder-module.c
+++ b/src/builder-module.c
@@ -2211,6 +2211,20 @@ builder_module_build_helper (BuilderModule *self,
}
}
+ for (GList *l = self->sources; l != NULL; l = l->next)
+ {
+ BuilderSource *source = l->data;
+
+ if (!builder_source_is_enabled (source, context))
+ continue;
+
+ if (!builder_source_install (source, source_dir, context, error))
+ {
+ g_prefix_error (error, "module %s: ", self->name);
+ return FALSE;
+ }
+ }
+
/* Run unit tests */
if (self->run_tests && builder_context_get_run_tests (context))
diff --git a/src/builder-source-file.c b/src/builder-source-file.c
index 25434491..35820efa 100644
--- a/src/builder-source-file.c
+++ b/src/builder-source-file.c
@@ -46,6 +46,8 @@ struct BuilderSourceFile
char *dest_filename;
char *http_referer;
gboolean disable_http_decompression;
+ char *install_dir;
+ char *install_mode;
};
typedef struct
@@ -67,6 +69,8 @@ enum {
PROP_MIRROR_URLS,
PROP_HTTP_REFERER,
PROP_DISABLE_HTTP_DECOMPRESSION,
+ PROP_INSTALL_DIR,
+ PROP_INSTALL_MODE,
LAST_PROP
};
@@ -84,6 +88,8 @@ builder_source_file_finalize (GObject *object)
g_free (self->dest_filename);
g_free (self->http_referer);
g_strfreev (self->mirror_urls);
+ g_free (self->install_dir);
+ g_free (self->install_mode);
G_OBJECT_CLASS (builder_source_file_parent_class)->finalize (object);
}
@@ -138,6 +144,14 @@ builder_source_file_get_property (GObject *object,
g_value_set_boolean (value, self->disable_http_decompression);
break;
+ case PROP_INSTALL_DIR:
+ g_value_set_string (value, self->install_dir);
+ break;
+
+ case PROP_INSTALL_MODE:
+ g_value_set_string (value, self->install_mode);
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
@@ -216,6 +230,16 @@ builder_source_file_set_property (GObject *object,
self->disable_http_decompression = g_value_get_boolean (value);
break;
+ case PROP_INSTALL_DIR:
+ g_free (self->install_dir);
+ self->install_dir = g_value_dup_string (value);
+ break;
+
+ case PROP_INSTALL_MODE:
+ g_free (self->install_mode);
+ self->install_mode = g_value_dup_string (value);
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
@@ -231,6 +255,20 @@ builder_source_file_validate (BuilderSource *source,
strchr (self->dest_filename, '/') != NULL)
return flatpak_fail (error, "No slashes allowed in dest-filename, use dest property for directory");
+ if (self->install_dir != NULL && g_path_is_absolute (self->install_dir))
+ return flatpak_fail (error, "install-dir must be relative to FLATPAK_DEST");
+
+ if (self->install_mode != NULL)
+ {
+ char *end;
+ guint64 mode = g_ascii_strtoull (self->install_mode, &end, 8);
+ if (*end != '\0' || mode > 07777)
+ return flatpak_fail (error, "install-mode must be a valid octal permission string");
+ }
+
+ if (self->install_mode != NULL && self->install_dir == NULL)
+ return flatpak_fail (error, "install-mode requires install-dir to be set");
+
return TRUE;
}
@@ -552,6 +590,87 @@ builder_source_file_extract (BuilderSource *source,
return TRUE;
}
+static gboolean
+builder_source_file_install (BuilderSource *source,
+ GFile *build_dir,
+ BuilderContext *context,
+ GError **error)
+{
+ BuilderSourceFile *self = BUILDER_SOURCE_FILE (source);
+ const char *filename;
+ g_autofree char *basename = NULL;
+ g_autofree char *dst_path = NULL;
+ g_autoptr(GFile) src = NULL;
+ g_autoptr(GFile) install_dir = NULL;
+ g_autoptr(GFile) dst = NULL;
+ g_autoptr(GFile) dest_dir = NULL;
+ GFile *app_dir = NULL;
+
+ if (self->install_dir == NULL || self->install_dir[0] == '\0')
+ return TRUE;
+
+ if (self->dest_filename)
+ filename = self->dest_filename;
+ else
+ {
+ gboolean is_local, is_inline;
+ g_autoptr(GFile) source_file = get_source_file (self, context, &is_local, &is_inline, error);
+ if (source_file == NULL)
+ return FALSE;
+ basename = g_file_get_basename (source_file);
+ filename = basename;
+ }
+
+ app_dir = builder_context_get_app_dir (context);
+ dest_dir = builder_context_get_build_runtime (context)
+ ? g_file_get_child (app_dir, "usr")
+ : g_file_get_child (app_dir, "files");
+ install_dir = g_file_resolve_relative_path (dest_dir, self->install_dir);
+
+ if (!g_file_has_prefix (install_dir, dest_dir))
+ return flatpak_fail (error, "install-dir cannot escape the install prefix");
+
+ src = g_file_get_child (build_dir, filename);
+ dst = g_file_get_child (install_dir, filename);
+ dst_path = g_file_get_path (dst);
+
+ if (!flatpak_file_query_exists_nofollow (src))
+ return flatpak_fail (error, "Source file '%s' not found", filename);
+
+ if (!flatpak_mkdir_p (install_dir, NULL, error))
+ return FALSE;
+
+ if (flatpak_file_query_exists_nofollow (dst))
+ g_printerr ("Warning: %s already exists, skipping install\n", dst_path);
+ else
+ {
+ if (!g_file_copy (src, dst, G_FILE_COPY_NOFOLLOW_SYMLINKS,
+ NULL, NULL, NULL, error))
+ return FALSE;
+
+ if (self->install_mode)
+ {
+ guint32 mode = (guint32) g_ascii_strtoull (self->install_mode, NULL, 8);
+
+ if (!g_file_set_attribute (dst,
+ G_FILE_ATTRIBUTE_UNIX_MODE,
+ G_FILE_ATTRIBUTE_TYPE_UINT32,
+ &mode,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+ NULL,
+ error))
+ return FALSE;
+ }
+
+ if (self->install_mode)
+ g_print ("Installed %s to %s with permissions %s\n", filename, dst_path, self->install_mode);
+ else
+ g_print ("Installed %s to %s with default permissions\n", filename, dst_path);
+ }
+
+ return TRUE;
+}
+
static gboolean
builder_source_file_bundle (BuilderSource *source,
BuilderContext *context,
@@ -655,6 +774,8 @@ builder_source_file_checksum (BuilderSource *source,
builder_cache_checksum_compat_str (cache, self->sha512);
builder_cache_checksum_str (cache, self->dest_filename);
builder_cache_checksum_compat_strv (cache, self->mirror_urls);
+ builder_cache_checksum_compat_str (cache, self->install_dir);
+ builder_cache_checksum_compat_str (cache, self->install_mode);
}
static void
@@ -670,6 +791,7 @@ builder_source_file_class_init (BuilderSourceFileClass *klass)
source_class->show_deps = builder_source_file_show_deps;
source_class->download = builder_source_file_download;
source_class->extract = builder_source_file_extract;
+ source_class->install = builder_source_file_install;
source_class->bundle = builder_source_file_bundle;
source_class->update = builder_source_file_update;
source_class->checksum = builder_source_file_checksum;
@@ -747,6 +869,20 @@ builder_source_file_class_init (BuilderSourceFileClass *klass)
"",
FALSE,
G_PARAM_READWRITE));
+ g_object_class_install_property (object_class,
+ PROP_INSTALL_DIR,
+ g_param_spec_string ("install-dir",
+ "",
+ "",
+ NULL,
+ G_PARAM_READWRITE));
+ g_object_class_install_property (object_class,
+ PROP_INSTALL_MODE,
+ g_param_spec_string ("install-mode",
+ "",
+ "",
+ NULL,
+ G_PARAM_READWRITE));
}
static void
diff --git a/src/builder-source.c b/src/builder-source.c
index 5047589a..4f319aa8 100644
--- a/src/builder-source.c
+++ b/src/builder-source.c
@@ -164,6 +164,15 @@ builder_source_real_extract (BuilderSource *self,
return FALSE;
}
+static gboolean
+builder_source_real_install (BuilderSource *self,
+ GFile *build_dir,
+ BuilderContext *context,
+ GError **error)
+{
+ return TRUE;
+}
+
static gboolean
builder_source_real_bundle (BuilderSource *self,
BuilderContext *context,
@@ -194,6 +203,7 @@ builder_source_class_init (BuilderSourceClass *klass)
klass->show_deps = builder_source_real_show_deps;
klass->download = builder_source_real_download;
klass->extract = builder_source_real_extract;
+ klass->install = builder_source_real_install;
klass->bundle = builder_source_real_bundle;
klass->update = builder_source_real_update;
@@ -415,6 +425,19 @@ builder_source_extract (BuilderSource *self,
return class->extract (self, real_dest, source_dir, build_options, context, error);
}
+gboolean
+builder_source_install (BuilderSource *self,
+ GFile *build_dir,
+ BuilderContext *context,
+ GError **error)
+{
+ BuilderSourceClass *class;
+
+ class = BUILDER_SOURCE_GET_CLASS (self);
+
+ return class->install (self, build_dir, context, error);
+}
+
gboolean
builder_source_bundle (BuilderSource *self,
BuilderContext *context,
diff --git a/src/builder-source.h b/src/builder-source.h
index b0e0b062..b937c0cc 100644
--- a/src/builder-source.h
+++ b/src/builder-source.h
@@ -63,6 +63,10 @@ typedef struct
BuilderOptions *build_options,
BuilderContext *context,
GError **error);
+ gboolean (* install)(BuilderSource *self,
+ GFile *build_dir,
+ BuilderContext *context,
+ GError **error);
gboolean (* bundle)(BuilderSource *self,
BuilderContext *context,
GError **error);
@@ -99,6 +103,10 @@ gboolean builder_source_extract (BuilderSource *self,
BuilderOptions *build_options,
BuilderContext *context,
GError **error);
+gboolean builder_source_install (BuilderSource *self,
+ GFile *build_dir,
+ BuilderContext *context,
+ GError **error);
gboolean builder_source_bundle (BuilderSource *self,
BuilderContext *context,
GError **error);
diff --git a/tests/test-builder.sh b/tests/test-builder.sh
index 16407856..0f0612e1 100755
--- a/tests/test-builder.sh
+++ b/tests/test-builder.sh
@@ -69,6 +69,7 @@ cp $(dirname $0)/source2.json include1/include2/
cp $(dirname $0)/data2 include1/include2/
cp $(dirname $0)/data2.patch include1/include2/
echo "MY LICENSE" > ./LICENSE
+touch ./foobar
for MANIFEST in test.json test.yaml test-rename.json test-rename-appdata.json ; do
echo "building manifest $MANIFEST" >&2
@@ -106,6 +107,13 @@ for MANIFEST in test.json test.yaml test-rename.json test-rename-appdata.json ;
assert_file_has_content appdir/files/share/licenses/org.test.Hello2/test/LICENSE '^MY LICENSE$'
+ assert_has_file appdir/files/share/icons/hicolor/64x64/apps/foobar
+ PERMS=$(stat -c "%a" appdir/files/share/icons/hicolor/64x64/apps/foobar)
+ if [ "$PERMS" != "755" ]; then
+ echo "not ok install-dir+install-mode: expected 755, got $PERMS"
+ exit 1
+ fi
+
echo "ok build"
done
diff --git a/tests/test-rename-appdata.json b/tests/test-rename-appdata.json
index 83650d8a..78c1fa79 100644
--- a/tests/test-rename-appdata.json
+++ b/tests/test-rename-appdata.json
@@ -82,6 +82,12 @@
"dest-filename": "hello2.sh",
"commands": [ "echo \"Hello world2, from a sandbox\"" ]
},
+ {
+ "type": "file",
+ "path": "foobar",
+ "install-dir": "share/icons/hicolor/64x64/apps",
+ "install-mode": "0755"
+ },
{
"type": "shell",
"commands": [
diff --git a/tests/test-rename.json b/tests/test-rename.json
index 6b171982..581428ab 100644
--- a/tests/test-rename.json
+++ b/tests/test-rename.json
@@ -83,6 +83,12 @@
"dest-filename": "hello2.sh",
"commands": [ "echo \"Hello world2, from a sandbox\"" ]
},
+ {
+ "type": "file",
+ "path": "foobar",
+ "install-dir": "share/icons/hicolor/64x64/apps",
+ "install-mode": "0755"
+ },
{
"type": "shell",
"commands": [
diff --git a/tests/test.json b/tests/test.json
index 532a13d8..ad638db2 100644
--- a/tests/test.json
+++ b/tests/test.json
@@ -75,6 +75,12 @@
"type": "file",
"path": "LICENSE"
},
+ {
+ "type": "file",
+ "path": "foobar",
+ "install-dir": "share/icons/hicolor/64x64/apps",
+ "install-mode": "0755"
+ },
{
"type": "shell",
"commands": [
diff --git a/tests/test.yaml b/tests/test.yaml
index 734523c8..227a1ef5 100644
--- a/tests/test.yaml
+++ b/tests/test.yaml
@@ -56,6 +56,10 @@ modules:
- type: file
path: LICENSE
dest: mytest
+ - type: file
+ path: foobar
+ install-dir: share/icons/hicolor/64x64/apps
+ install-mode: "0755"
- type: shell
commands:
- mkdir /app/cleanup/