Add pin command to keep unused runtimes

As discussed here [1], we want a way to mark runtimes to be kept even
when they are unused by any apps and we are removing such runtimes.
Currently this is a command that can be run manually; a subsequent
commit will pin runtimes automatically if they are installed
independently of any app.

A unit test is included.

[1] https://github.com/flatpak/flatpak/issues/2639#issuecomment-662311756
This commit is contained in:
Matthew Leeds
2020-07-30 17:05:43 -07:00
committed by Alexander Larsson
parent f24b1fdc1a
commit d2d5397cc1
15 changed files with 472 additions and 10 deletions

View File

@@ -81,6 +81,7 @@ flatpak_SOURCES = \
app/flatpak-builtins-update.c \
app/flatpak-builtins-uninstall.c \
app/flatpak-builtins-mask.c \
app/flatpak-builtins-pin.c \
app/flatpak-builtins-list.c \
app/flatpak-builtins-info.c \
app/flatpak-builtins-config.c \

View File

@@ -138,7 +138,9 @@ flatpak_builtin_mask (int argc, char **argv, GCancellable *cancellable, GError *
{
g_autofree char *regexp;
regexp = flatpak_filter_glob_to_regexp (pattern, error);
regexp = flatpak_filter_glob_to_regexp (pattern,
FALSE, /* match apps or runtimes */
error);
if (regexp == NULL)
return FALSE;

185
app/flatpak-builtins-pin.c Normal file
View File

@@ -0,0 +1,185 @@
/*
* Copyright © 2020 Endless OS Foundation LLC
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Matthew Leeds <matthew.leeds@endlessm.com>
*/
#include "config.h"
#include <locale.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <glib/gi18n.h>
#include "libglnx/libglnx.h"
#include "flatpak-builtins.h"
#include "flatpak-builtins-utils.h"
#include "flatpak-cli-transaction.h"
#include "flatpak-quiet-transaction.h"
#include "flatpak-utils-private.h"
#include "flatpak-error.h"
/* Note: the code here is copied from flatpak-builtins-mask.c */
static gboolean opt_remove;
static GOptionEntry options[] = {
{ "remove", 0, 0, G_OPTION_ARG_NONE, &opt_remove, N_("Remove matching pins"), NULL },
{ NULL }
};
static GPtrArray *
get_old_patterns (FlatpakDir *dir)
{
g_autoptr(GPtrArray) patterns = NULL;
g_autofree char *pinned = NULL;
int i;
patterns = g_ptr_array_new_with_free_func (g_free);
pinned = flatpak_dir_get_config (dir, "pinned", NULL);
if (pinned)
{
g_auto(GStrv) oldv = g_strsplit (pinned, ";", -1);
for (i = 0; oldv[i] != NULL; i++)
{
const char *old = oldv[i];
if (*old != 0 && !flatpak_g_ptr_array_contains_string (patterns, old))
g_ptr_array_add (patterns, g_strdup (old));
}
}
return g_steal_pointer (&patterns);
}
gboolean
flatpak_builtin_pin (int argc, char **argv, GCancellable *cancellable, GError **error)
{
g_autoptr(GOptionContext) context = NULL;
g_autoptr(GPtrArray) dirs = NULL;
FlatpakDir *dir;
g_autofree char *merged_patterns = NULL;
g_autoptr(GPtrArray) patterns = NULL;
int i;
context = g_option_context_new (_("[PATTERN…] - disable automatic removal of runtimes matching patterns"));
g_option_context_set_translation_domain (context, GETTEXT_PACKAGE);
if (!flatpak_option_context_parse (context, options, &argc, &argv,
FLATPAK_BUILTIN_FLAG_ONE_DIR,
&dirs, cancellable, error))
return FALSE;
dir = g_ptr_array_index (dirs, 0);
patterns = get_old_patterns (dir);
if (argc == 1)
{
if (patterns->len == 0)
{
g_print (_("No pinned patterns\n"));
}
else
{
g_print (_("Pinned patterns:\n"));
for (i = 0; i < patterns->len; i++)
{
const char *old = g_ptr_array_index (patterns, i);
g_print (" %s\n", old);
}
}
}
else
{
for (i = 1; i < argc; i++)
{
const char *pattern = argv[i];
if (opt_remove)
{
int j;
for (j = 0; j < patterns->len; j++)
{
if (strcmp (g_ptr_array_index (patterns, j), pattern) == 0)
break;
}
if (j == patterns->len)
return flatpak_fail (error, _("No current pin matching %s"), pattern);
else
g_ptr_array_remove_index (patterns, j);
}
else
{
g_autofree char *regexp;
regexp = flatpak_filter_glob_to_regexp (pattern,
TRUE, /* only match runtimes */
error);
if (regexp == NULL)
return FALSE;
if (!flatpak_g_ptr_array_contains_string (patterns, pattern))
g_ptr_array_add (patterns, g_strdup (pattern));
}
}
g_ptr_array_sort (patterns, flatpak_strcmp0_ptr);
g_ptr_array_add (patterns, NULL);
merged_patterns = g_strjoinv (";", (char **)patterns->pdata);
if (!flatpak_dir_set_config (dir, "pinned", merged_patterns, error))
return FALSE;
}
return TRUE;
}
gboolean
flatpak_complete_pin (FlatpakCompletion *completion)
{
g_autoptr(GOptionContext) context = NULL;
g_autoptr(GPtrArray) dirs = NULL;
context = g_option_context_new ("");
if (!flatpak_option_context_parse (context, options, &completion->argc, &completion->argv,
FLATPAK_BUILTIN_FLAG_ONE_DIR | FLATPAK_BUILTIN_FLAG_OPTIONAL_REPO,
&dirs, NULL, NULL))
return FALSE;
switch (completion->argc)
{
case 0:
case 1: /* PATTERN */
flatpak_complete_options (completion, global_entries);
flatpak_complete_options (completion, options);
flatpak_complete_options (completion, user_entries);
break;
}
return TRUE;
}

View File

@@ -86,6 +86,7 @@ BUILTINPROTO (remote_info)
BUILTINPROTO (remote_list)
BUILTINPROTO (install)
BUILTINPROTO (mask)
BUILTINPROTO (pin)
BUILTINPROTO (update)
BUILTINPROTO (make_current_app)
BUILTINPROTO (uninstall)

View File

@@ -80,6 +80,7 @@ static FlatpakCommand commands[] = {
/* Alias remove to uninstall to help users of yum/dnf/apt */
{ "remove", NULL, flatpak_builtin_uninstall, flatpak_complete_uninstall, TRUE },
{ "mask", N_("Mask out updates and automatic installation"), flatpak_builtin_mask, flatpak_complete_mask },
{ "pin", N_("Pin a runtime to prevent automatic removal"), flatpak_builtin_pin, flatpak_complete_pin },
{ "list", N_("List installed apps and/or runtimes"), flatpak_builtin_list, flatpak_complete_list },
{ "info", N_("Show info for installed app or runtime"), flatpak_builtin_info, flatpak_complete_info },
{ "history", N_("Show history"), flatpak_builtin_history, flatpak_complete_history },

View File

@@ -474,6 +474,8 @@ char ** flatpak_dir_search_for_dependency (FlatpakDir *self,
GError **error);
gboolean flatpak_dir_ref_is_masked (FlatpakDir *self,
const char *ref);
gboolean flatpak_dir_ref_is_pinned (FlatpakDir *self,
const char *ref);
char * flatpak_dir_find_remote_ref (FlatpakDir *self,
const char *remote,
const char **opt_sideload_repos,

View File

@@ -214,6 +214,7 @@ struct FlatpakDir
/* Config cache, protected by config_cache lock */
GRegex *masked;
GRegex *pinned;
SoupSession *soup_session;
};
@@ -2260,6 +2261,7 @@ flatpak_dir_finalize (GObject *object)
g_clear_pointer (&self->summary_cache, g_hash_table_unref);
g_clear_pointer (&self->remote_filters, g_hash_table_unref);
g_clear_pointer (&self->masked, g_regex_unref);
g_clear_pointer (&self->pinned, g_regex_unref);
G_OBJECT_CLASS (flatpak_dir_parent_class)->finalize (object);
}
@@ -3436,6 +3438,7 @@ flatpak_dir_recreate_repo (FlatpakDir *self,
G_LOCK (config_cache);
g_clear_pointer (&self->masked, g_regex_unref);
g_clear_pointer (&self->pinned, g_regex_unref);
G_UNLOCK (config_cache);
@@ -3841,6 +3844,7 @@ _flatpak_dir_reload_config (FlatpakDir *self,
G_LOCK (config_cache);
g_clear_pointer (&self->masked, g_regex_unref);
g_clear_pointer (&self->pinned, g_regex_unref);
G_UNLOCK (config_cache);
return TRUE;
@@ -13603,7 +13607,7 @@ flatpak_dir_get_mask_regexp (FlatpakDir *self)
{
g_autofree char *regexp = NULL;
regexp = flatpak_filter_glob_to_regexp (pattern, NULL);
regexp = flatpak_filter_glob_to_regexp (pattern, FALSE, NULL);
if (regexp)
{
if (i != 0)
@@ -13635,6 +13639,66 @@ flatpak_dir_ref_is_masked (FlatpakDir *self,
return !flatpak_filters_allow_ref (NULL, masked, ref);
}
static GRegex *
flatpak_dir_get_pin_regexp (FlatpakDir *self)
{
GRegex *res = NULL;
G_LOCK (config_cache);
if (self->pinned == NULL)
{
g_autofree char *pinned = NULL;
pinned = flatpak_dir_get_config (self, "pinned", NULL);
if (pinned)
{
g_auto(GStrv) patterns = g_strsplit (pinned, ";", -1);
g_autoptr(GString) deny_regexp = g_string_new ("^(");
int i;
for (i = 0; patterns[i] != NULL; i++)
{
const char *pattern = patterns[i];
if (*pattern != 0)
{
g_autofree char *regexp = NULL;
regexp = flatpak_filter_glob_to_regexp (pattern,
TRUE, /* only match runtimes */
NULL);
if (regexp)
{
if (i != 0)
g_string_append (deny_regexp, "|");
g_string_append (deny_regexp, regexp);
}
}
}
g_string_append (deny_regexp, ")$");
self->pinned = g_regex_new (deny_regexp->str, G_REGEX_DOLLAR_ENDONLY|G_REGEX_RAW|G_REGEX_OPTIMIZE, G_REGEX_MATCH_ANCHORED, NULL);
}
}
if (self->pinned)
res = g_regex_ref (self->pinned);
G_UNLOCK (config_cache);
return res;
}
gboolean
flatpak_dir_ref_is_pinned (FlatpakDir *self,
const char *ref)
{
g_autoptr(GRegex) pinned = flatpak_dir_get_pin_regexp (self);
return !flatpak_filters_allow_ref (NULL, pinned, ref);
}
GPtrArray *
flatpak_dir_find_remote_related_for_metadata (FlatpakDir *self,
FlatpakRemoteState *state,

View File

@@ -2926,6 +2926,7 @@ find_used_refs (FlatpakDir *dir,
*
* A reference is used if it is either an application, or an sdk,
* or the runtime of a used ref, or an extension of a used ref.
* Pinned runtimes are also considered used; see flatpak-pin(1).
*
* Returns: (transfer container) (element-type FlatpakInstalledRef): a GPtrArray of
* #FlatpakInstalledRef instances
@@ -3030,6 +3031,12 @@ flatpak_installation_list_unused_refs (FlatpakInstallation *self,
if (arch != NULL && strcmp (parts[2], arch) != 0)
continue;
if (flatpak_dir_ref_is_pinned (dir, ref))
{
g_debug ("Ref %s is pinned, considering as used", ref);
continue;
}
if (!g_hash_table_contains (used_refs, ref))
{
if (g_hash_table_add (refs_hash, (gpointer) ref))

View File

@@ -194,7 +194,7 @@ gboolean flatpak_id_has_subref_suffix (const char *id);
char **flatpak_decompose_ref (const char *ref,
GError **error);
char * flatpak_filter_glob_to_regexp (const char *glob, GError **error);
char * flatpak_filter_glob_to_regexp (const char *glob, gboolean runtime_only, GError **error);
gboolean flatpak_parse_filters (const char *data,
GRegex **allow_refs_out,
GRegex **deny_refs_out,

View File

@@ -1131,7 +1131,9 @@ line_get_word (char **line)
}
char *
flatpak_filter_glob_to_regexp (const char *glob, GError **error)
flatpak_filter_glob_to_regexp (const char *glob,
gboolean runtime_only,
GError **error)
{
g_autoptr(GString) regexp = g_string_new ("");
int parts = 1;
@@ -1139,8 +1141,16 @@ flatpak_filter_glob_to_regexp (const char *glob, GError **error)
if (g_str_has_prefix (glob, "app/"))
{
glob += strlen ("app/");
g_string_append (regexp, "app/");
if (runtime_only)
{
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Glob can't match apps"));
return NULL;
}
else
{
glob += strlen ("app/");
g_string_append (regexp, "app/");
}
}
else if (g_str_has_prefix (glob, "runtime/"))
{
@@ -1148,7 +1158,12 @@ flatpak_filter_glob_to_regexp (const char *glob, GError **error)
g_string_append (regexp, "runtime/");
}
else
g_string_append (regexp, "(app|runtime)/");
{
if (runtime_only)
g_string_append (regexp, "runtime/");
else
g_string_append (regexp, "(app|runtime)/");
}
/* We really need an id part, the rest is optional */
if (*glob == 0)
@@ -1253,7 +1268,7 @@ flatpak_parse_filters (const char *data,
if (next != NULL)
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Trailing text on line %d"), i + 1);
ref_regexp = flatpak_filter_glob_to_regexp (glob, error);
ref_regexp = flatpak_filter_glob_to_regexp (glob, FALSE, error);
if (ref_regexp == NULL)
return glnx_prefix_error (error, _("on line %d"), i + 1);

View File

@@ -30,6 +30,7 @@ man1 = \
flatpak-update.1 \
flatpak-uninstall.1 \
flatpak-mask.1 \
flatpak-pin.1 \
flatpak-list.1 \
flatpak-info.1 \
flatpak-make-current.1 \

151
doc/flatpak-pin.xml Normal file
View File

@@ -0,0 +1,151 @@
<?xml version='1.0'?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
<refentry id="flatpak-pin">
<refentryinfo>
<title>flatpak pin</title>
<productname>flatpak</productname>
<authorgroup>
<author>
<contrib>Developer</contrib>
<firstname>Matthew</firstname>
<surname>Leeds</surname>
<email>matthew.leeds@endlessm.com</email>
</author>
</authorgroup>
</refentryinfo>
<refmeta>
<refentrytitle>flatpak pin</refentrytitle>
<manvolnum>1</manvolnum>
</refmeta>
<refnamediv>
<refname>flatpak-pin</refname>
<refpurpose>Pin runtimes to prevent automatic removal</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>flatpak pin</command>
<arg choice="opt" rep="repeat">OPTION</arg>
<arg choice="plain" rep="repeat">PATTERN</arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para>
Flatpak maintains a list of patterns that define which refs are pinned.
A pinned ref will never be automatically uninstalled (as are unused
runtimes periodically). This can be useful if for example you are using
a runtime for development purposes.
</para>
<para>
The patterns are just a partial ref, with the * character matching anything
within that part of the ref. Only runtimes can be pinned, not apps. Here
are some example patterns:
<programlisting>
org.some.Runtime
org.some.Runtime//unstable
runtime/org.domain.*
org.some.Runtime/arm
</programlisting>
</para>
<para>
To list the current set of pins, run this command without any patterns.
</para>
</refsect1>
<refsect1>
<title>Options</title>
<para>The following options are understood:</para>
<variablelist>
<varlistentry>
<term><option>-h</option></term>
<term><option>--help</option></term>
<listitem><para>
Show help options and exit.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--remove</option></term>
<listitem><para>
Instead of adding the patterns, remove matching patterns.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--user</option></term>
<listitem><para>
Pin refs in a per-user installation.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--system</option></term>
<listitem><para>
Pin refs in the default system-wide installation.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--installation=NAME</option></term>
<listitem><para>
Pin refs in a system-wide installation
specified by <arg choice="plain">NAME</arg> among those defined in
<filename>/etc/flatpak/installations.d/</filename>. Using
<option>--installation=default</option> is equivalent to using
<option>--system</option>.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>-v</option></term>
<term><option>--verbose</option></term>
<listitem><para>
Print debug information during command processing.
</para></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Examples</title>
<para>
<command>$ flatpak pin</command>
</para>
<para>
<command>$ flatpak pin org.freedesktop.Platform//19.08</command>
</para>
<para>
<command>$ flatpak pin --remove org.freedesktop.Platform//19.08</command>
</para>
</refsect1>
<refsect1>
<title>See also</title>
<para>
<citerefentry><refentrytitle>flatpak</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>flatpak-uninstall</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
</para>
</refsect1>
</refentry>

View File

@@ -215,6 +215,13 @@
Mask out updates and automatic installation
</para></listitem>
</varlistentry>
<varlistentry>
<term><citerefentry><refentrytitle>flatpak-pin</refentrytitle><manvolnum>1</manvolnum></citerefentry></term>
<listitem><para>
Pin runtimes to prevent automatic removal.
</para></listitem>
</varlistentry>
<varlistentry>
<term><citerefentry><refentrytitle>flatpak-list</refentrytitle><manvolnum>1</manvolnum></citerefentry></term>

View File

@@ -1108,7 +1108,8 @@ handle_configure (FlatpakSystemHelper *object,
if ((strcmp (arg_key, "languages") != 0) &&
(strcmp (arg_key, "extra-languages") != 0) &&
(strcmp (arg_key, "masked") != 0))
(strcmp (arg_key, "masked") != 0) &&
(strcmp (arg_key, "pinned") != 0))
{
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
"Unsupported key: %s", arg_key);

View File

@@ -24,7 +24,7 @@ set -euo pipefail
skip_without_bwrap
skip_revokefs_without_fuse
echo "1..37"
echo "1..38"
#Regular repo
setup_repo
@@ -587,6 +587,30 @@ assert_not_file_has_content list-log "org\.test\.Platform"
ok "uninstall --unused"
${FLATPAK} ${U} install -y test-repo org.test.Platform
${FLATPAK} ${U} list -a --columns=application > list-log
assert_file_has_content list-log "org\.test\.Platform"
# Check that the runtime won't be removed if it's pinned
${FLATPAK} ${U} pin org.test.Platform
${FLATPAK} ${U} pin > pins
assert_file_has_content pins "org\.test\.Platform"
rm pins
${FLATPAK} ${U} uninstall -y --unused
${FLATPAK} ${U} list -a --columns=application > list-log
assert_file_has_content list-log "org\.test\.Platform"
# Remove the pin and try again
${FLATPAK} ${U} pin --remove "org.test.Platform"
${FLATPAK} ${U} uninstall -y --unused
${FLATPAK} ${U} list -a --columns=application > list-log
assert_not_file_has_content list-log "org\.test\.Platform"
ok "uninstall --unused ignores pinned runtimes"
# Test that remote-ls works in all of the following cases:
# * system remote, and --system is used
# * system remote, and --system is omitted