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/