Accepting request 211329 from home:michael-chang:btrfs-snapshot-booting
- add new patches for booting btrfs snapshot (fate#316522) (fate#316232) * 0001-btrfs-rename-skip_default-to-follow_default.patch * 0002-btrfs-add-ability-to-boot-from-subvolumes.patch * 0003-cmdline-add-envvar-loader_cmdline_append.patch * 0004-btrfs-export-subvolume-envvars.patch OBS-URL: https://build.opensuse.org/request/show/211329 OBS-URL: https://build.opensuse.org/package/show/Base:System/grub2?expand=0&rev=60
This commit is contained in:
parent
9df8033f58
commit
618ccb4258
85
0001-btrfs-rename-skip_default-to-follow_default.patch
Normal file
85
0001-btrfs-rename-skip_default-to-follow_default.patch
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
From: Jeff Mahoney <jeffm@suse.com>
|
||||||
|
Subject: grub2/btrfs: rename skip_default to follow_default
|
||||||
|
|
||||||
|
The skip_default code in find_path uses 1 to indicate that the default
|
||||||
|
should not be skipped, which is confusing. Let's rename that to
|
||||||
|
follow_default.
|
||||||
|
|
||||||
|
Signed-off-by: Jeff Mahoney <jeffm@suse.com>
|
||||||
|
---
|
||||||
|
grub-core/fs/btrfs.c | 20 ++++++++++----------
|
||||||
|
1 file changed, 10 insertions(+), 10 deletions(-)
|
||||||
|
|
||||||
|
--- a/grub-core/fs/btrfs.c 2013-06-16 08:24:05.000000000 -0400
|
||||||
|
+++ b/grub-core/fs/btrfs.c 2013-07-29 09:26:07.421380716 -0400
|
||||||
|
@@ -1208,7 +1208,7 @@ find_path (struct grub_btrfs_data *data,
|
||||||
|
grub_size_t allocated = 0;
|
||||||
|
struct grub_btrfs_dir_item *direl = NULL;
|
||||||
|
struct grub_btrfs_key key_out;
|
||||||
|
- int skip_default;
|
||||||
|
+ int follow_default;
|
||||||
|
const char *ctoken;
|
||||||
|
grub_size_t ctokenlen;
|
||||||
|
char *path_alloc = NULL;
|
||||||
|
@@ -1220,14 +1220,14 @@ find_path (struct grub_btrfs_data *data,
|
||||||
|
key->object_id = data->sblock.root_dir_objectid;
|
||||||
|
key->type = GRUB_BTRFS_ITEM_TYPE_DIR_ITEM;
|
||||||
|
key->offset = 0;
|
||||||
|
- skip_default = 1;
|
||||||
|
+ follow_default = 1;
|
||||||
|
origpath = grub_strdup (path);
|
||||||
|
if (!origpath)
|
||||||
|
return grub_errno;
|
||||||
|
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
- if (!skip_default)
|
||||||
|
+ if (!follow_default)
|
||||||
|
{
|
||||||
|
while (path[0] == '/')
|
||||||
|
path++;
|
||||||
|
@@ -1254,9 +1254,9 @@ find_path (struct grub_btrfs_data *data,
|
||||||
|
|
||||||
|
if (ctokenlen == 1 && ctoken[0] == '.')
|
||||||
|
{
|
||||||
|
- if (!skip_default)
|
||||||
|
+ if (!follow_default)
|
||||||
|
path = slash;
|
||||||
|
- skip_default = 0;
|
||||||
|
+ follow_default = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ctokenlen == 2 && ctoken[0] == '.' && ctoken[1] == '.')
|
||||||
|
@@ -1287,9 +1287,9 @@ find_path (struct grub_btrfs_data *data,
|
||||||
|
*type = GRUB_BTRFS_DIR_ITEM_TYPE_DIRECTORY;
|
||||||
|
key->object_id = key_out.offset;
|
||||||
|
|
||||||
|
- if (!skip_default)
|
||||||
|
+ if (!follow_default)
|
||||||
|
path = slash;
|
||||||
|
- skip_default = 0;
|
||||||
|
+ follow_default = 0;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
@@ -1359,9 +1359,9 @@ find_path (struct grub_btrfs_data *data,
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
- if (!skip_default)
|
||||||
|
+ if (!follow_default)
|
||||||
|
path = slash;
|
||||||
|
- skip_default = 0;
|
||||||
|
+ follow_default = 0;
|
||||||
|
if (cdirel->type == GRUB_BTRFS_DIR_ITEM_TYPE_SYMLINK)
|
||||||
|
{
|
||||||
|
struct grub_btrfs_inode inode;
|
||||||
|
@@ -1416,7 +1416,7 @@ find_path (struct grub_btrfs_data *data,
|
||||||
|
key->object_id = data->sblock.root_dir_objectid;
|
||||||
|
key->type = GRUB_BTRFS_ITEM_TYPE_DIR_ITEM;
|
||||||
|
key->offset = 0;
|
||||||
|
- skip_default = 1;
|
||||||
|
+ follow_default = 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
659
0002-btrfs-add-ability-to-boot-from-subvolumes.patch
Normal file
659
0002-btrfs-add-ability-to-boot-from-subvolumes.patch
Normal file
@ -0,0 +1,659 @@
|
|||||||
|
From: Jeff Mahoney <jeffm@suse.com>
|
||||||
|
Subject: grub2/btrfs: Add ability to boot from subvolumes
|
||||||
|
|
||||||
|
This patch adds the ability to specify a different root on a btrfs
|
||||||
|
filesystem too boot from other than the default one.
|
||||||
|
|
||||||
|
btrfs-list-snapshots <dev> will list the subvolumes available on the
|
||||||
|
filesystem.
|
||||||
|
|
||||||
|
set btrfs_subvol=<path> and set btrfs_subvolid=<subvolid> will specify
|
||||||
|
which subvolume to use and any pathnames provided with either of those
|
||||||
|
variables set will start using that root. If the subvolume or subvolume id
|
||||||
|
doesn't exist, then an error case will result.
|
||||||
|
|
||||||
|
It is possible to boot into a separate GRUB instance by exporting the
|
||||||
|
variable and loading the config file from the subvolume.
|
||||||
|
|
||||||
|
Signed-off-by: Jeff Mahoney <jeffm@suse.com>
|
||||||
|
---
|
||||||
|
|
||||||
|
grub-core/fs/btrfs.c | 529 +++++++++++++++++++++++++++++++++++++++++++++++++--
|
||||||
|
1 file changed, 518 insertions(+), 11 deletions(-)
|
||||||
|
|
||||||
|
--- a/grub-core/fs/btrfs.c 2013-07-29 09:26:01.585376955 -0400
|
||||||
|
+++ b/grub-core/fs/btrfs.c 2013-07-29 09:26:02.513377553 -0400
|
||||||
|
@@ -28,6 +28,9 @@
|
||||||
|
#include <grub/deflate.h>
|
||||||
|
#include <minilzo.h>
|
||||||
|
#include <grub/i18n.h>
|
||||||
|
+#include <grub/command.h>
|
||||||
|
+#include <grub/env.h>
|
||||||
|
+#include <grub/extcmd.h>
|
||||||
|
|
||||||
|
GRUB_MOD_LICENSE ("GPLv3+");
|
||||||
|
|
||||||
|
@@ -63,9 +66,11 @@ struct grub_btrfs_superblock
|
||||||
|
grub_uint64_t generation;
|
||||||
|
grub_uint64_t root_tree;
|
||||||
|
grub_uint64_t chunk_tree;
|
||||||
|
- grub_uint8_t dummy2[0x20];
|
||||||
|
+ grub_uint8_t dummy2[0x18];
|
||||||
|
+ grub_uint64_t bytes_used;
|
||||||
|
grub_uint64_t root_dir_objectid;
|
||||||
|
- grub_uint8_t dummy3[0x41];
|
||||||
|
+ grub_uint64_t num_devices;
|
||||||
|
+ grub_uint8_t dummy3[0x39];
|
||||||
|
struct grub_btrfs_device this_device;
|
||||||
|
char label[0x100];
|
||||||
|
grub_uint8_t dummy4[0x100];
|
||||||
|
@@ -104,6 +109,7 @@ struct grub_btrfs_data
|
||||||
|
grub_uint64_t exttree;
|
||||||
|
grub_size_t extsize;
|
||||||
|
struct grub_btrfs_extent_data *extent;
|
||||||
|
+ grub_uint64_t fs_tree;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum
|
||||||
|
@@ -113,6 +119,7 @@ enum
|
||||||
|
GRUB_BTRFS_ITEM_TYPE_DIR_ITEM = 0x54,
|
||||||
|
GRUB_BTRFS_ITEM_TYPE_EXTENT_ITEM = 0x6c,
|
||||||
|
GRUB_BTRFS_ITEM_TYPE_ROOT_ITEM = 0x84,
|
||||||
|
+ GRUB_BTRFS_ITEM_TYPE_ROOT_REF = 0x9c,
|
||||||
|
GRUB_BTRFS_ITEM_TYPE_DEVICE = 0xd8,
|
||||||
|
GRUB_BTRFS_ITEM_TYPE_CHUNK = 0xe4
|
||||||
|
};
|
||||||
|
@@ -195,6 +202,21 @@ struct grub_btrfs_root_item
|
||||||
|
grub_uint64_t inode;
|
||||||
|
};
|
||||||
|
|
||||||
|
+struct grub_btrfs_inode_ref
|
||||||
|
+{
|
||||||
|
+ grub_uint64_t index;
|
||||||
|
+ grub_uint16_t name_len;
|
||||||
|
+ const char name[0];
|
||||||
|
+} __attribute__ ((packed));
|
||||||
|
+
|
||||||
|
+struct grub_btrfs_root_ref
|
||||||
|
+{
|
||||||
|
+ grub_uint64_t dirid;
|
||||||
|
+ grub_uint64_t sequence;
|
||||||
|
+ grub_uint16_t name_len;
|
||||||
|
+ const char name[0];
|
||||||
|
+} __attribute__ ((packed));
|
||||||
|
+
|
||||||
|
struct grub_btrfs_time
|
||||||
|
{
|
||||||
|
grub_int64_t sec;
|
||||||
|
@@ -239,6 +261,14 @@ struct grub_btrfs_extent_data
|
||||||
|
|
||||||
|
#define GRUB_BTRFS_OBJECT_ID_CHUNK 0x100
|
||||||
|
|
||||||
|
+#define GRUB_BTRFS_ROOT_TREE_OBJECTID 1ULL
|
||||||
|
+#define GRUB_BTRFS_FS_TREE_OBJECTID 5ULL
|
||||||
|
+#define GRUB_BTRFS_ROOT_REF_KEY 156
|
||||||
|
+#define GRUB_BTRFS_ROOT_ITEM_KEY 132
|
||||||
|
+
|
||||||
|
+static grub_uint64_t btrfs_default_subvolid = 0;
|
||||||
|
+static char *btrfs_default_subvol = NULL;
|
||||||
|
+
|
||||||
|
static grub_disk_addr_t superblock_sectors[] = { 64 * 2, 64 * 1024 * 2,
|
||||||
|
256 * 1048576 * 2, 1048576ULL * 1048576ULL * 2
|
||||||
|
};
|
||||||
|
@@ -854,6 +884,62 @@ grub_btrfs_read_logical (struct grub_btr
|
||||||
|
return GRUB_ERR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
+static grub_err_t
|
||||||
|
+get_fs_root(struct grub_btrfs_data *data, grub_uint64_t tree,
|
||||||
|
+ grub_uint64_t objectid, grub_uint64_t offset,
|
||||||
|
+ grub_uint64_t *fs_root);
|
||||||
|
+
|
||||||
|
+static grub_err_t
|
||||||
|
+lookup_root_by_id(struct grub_btrfs_data *data, grub_uint64_t id)
|
||||||
|
+{
|
||||||
|
+ grub_err_t err;
|
||||||
|
+ grub_uint64_t tree;
|
||||||
|
+
|
||||||
|
+ err = get_fs_root(data, data->sblock.root_tree, id, -1, &tree);
|
||||||
|
+ if (!err)
|
||||||
|
+ data->fs_tree = tree;
|
||||||
|
+ return err;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static grub_err_t
|
||||||
|
+find_path (struct grub_btrfs_data *data,
|
||||||
|
+ const char *path, struct grub_btrfs_key *key,
|
||||||
|
+ grub_uint64_t *tree, grub_uint8_t *type);
|
||||||
|
+
|
||||||
|
+static grub_err_t
|
||||||
|
+lookup_root_by_name(struct grub_btrfs_data *data, const char *path)
|
||||||
|
+{
|
||||||
|
+ grub_err_t err;
|
||||||
|
+ grub_uint64_t tree = 0;
|
||||||
|
+ grub_uint8_t type;
|
||||||
|
+ struct grub_btrfs_key key;
|
||||||
|
+
|
||||||
|
+ err = find_path (data, path, &key, &tree, &type);
|
||||||
|
+ if (err)
|
||||||
|
+ return grub_error(GRUB_ERR_FILE_NOT_FOUND, "couldn't locate %s\n", path);
|
||||||
|
+
|
||||||
|
+ if (key.object_id != GRUB_BTRFS_OBJECT_ID_CHUNK || tree == 0)
|
||||||
|
+ return grub_error(GRUB_ERR_BAD_FILE_TYPE, "%s: not a subvolume\n", path);
|
||||||
|
+
|
||||||
|
+ data->fs_tree = tree;
|
||||||
|
+ return GRUB_ERR_NONE;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static grub_err_t
|
||||||
|
+btrfs_handle_subvol(struct grub_btrfs_data *data __attribute__ ((unused)))
|
||||||
|
+{
|
||||||
|
+ if (btrfs_default_subvol)
|
||||||
|
+ return lookup_root_by_name(data, btrfs_default_subvol);
|
||||||
|
+
|
||||||
|
+ if (btrfs_default_subvolid)
|
||||||
|
+ return lookup_root_by_id(data, btrfs_default_subvolid);
|
||||||
|
+
|
||||||
|
+ data->fs_tree = 0;
|
||||||
|
+
|
||||||
|
+ return GRUB_ERR_NONE;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+
|
||||||
|
static struct grub_btrfs_data *
|
||||||
|
grub_btrfs_mount (grub_device_t dev)
|
||||||
|
{
|
||||||
|
@@ -889,6 +975,13 @@ grub_btrfs_mount (grub_device_t dev)
|
||||||
|
data->devices_attached[0].dev = dev;
|
||||||
|
data->devices_attached[0].id = data->sblock.this_device.device_id;
|
||||||
|
|
||||||
|
+ err = btrfs_handle_subvol (data);
|
||||||
|
+ if (err)
|
||||||
|
+ {
|
||||||
|
+ grub_free (data);
|
||||||
|
+ return NULL;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -1197,6 +1290,91 @@ grub_btrfs_extent_read (struct grub_btrf
|
||||||
|
}
|
||||||
|
|
||||||
|
static grub_err_t
|
||||||
|
+find_pathname(struct grub_btrfs_data *data, grub_uint64_t objectid,
|
||||||
|
+ grub_uint64_t fs_root, const char *name, char **pathname)
|
||||||
|
+{
|
||||||
|
+ grub_err_t err;
|
||||||
|
+ struct grub_btrfs_key key = {
|
||||||
|
+ .object_id = objectid,
|
||||||
|
+ .type = GRUB_BTRFS_ITEM_TYPE_INODE_REF,
|
||||||
|
+ .offset = 0,
|
||||||
|
+ };
|
||||||
|
+ struct grub_btrfs_key key_out;
|
||||||
|
+ struct grub_btrfs_leaf_descriptor desc;
|
||||||
|
+ char *p = grub_strdup (name);
|
||||||
|
+ grub_disk_addr_t elemaddr;
|
||||||
|
+ grub_size_t elemsize;
|
||||||
|
+ int alloc = grub_strlen(name) + 1;
|
||||||
|
+
|
||||||
|
+ err = lower_bound(data, &key, &key_out, fs_root,
|
||||||
|
+ &elemaddr, &elemsize, &desc, 0);
|
||||||
|
+ if (err)
|
||||||
|
+ return grub_error(err, "lower_bound caught %d\n", err);
|
||||||
|
+
|
||||||
|
+ if (key_out.type != GRUB_BTRFS_ITEM_TYPE_INODE_REF)
|
||||||
|
+ next(data, &desc, &elemaddr, &elemsize, &key_out);
|
||||||
|
+
|
||||||
|
+ if (key_out.type != GRUB_BTRFS_ITEM_TYPE_INODE_REF)
|
||||||
|
+ {
|
||||||
|
+ return grub_error(GRUB_ERR_FILE_NOT_FOUND,
|
||||||
|
+ "Can't find inode ref for {%"PRIuGRUB_UINT64_T
|
||||||
|
+ ", %u, %"PRIuGRUB_UINT64_T"} %"PRIuGRUB_UINT64_T
|
||||||
|
+ "/%"PRIuGRUB_SIZE"\n",
|
||||||
|
+ key_out.object_id, key_out.type,
|
||||||
|
+ key_out.offset, elemaddr, elemsize);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+ while (key_out.type == GRUB_BTRFS_ITEM_TYPE_INODE_REF &&
|
||||||
|
+ key_out.object_id != key_out.offset) {
|
||||||
|
+ struct grub_btrfs_inode_ref *inode_ref;
|
||||||
|
+ char *new;
|
||||||
|
+
|
||||||
|
+ inode_ref = grub_malloc(elemsize + 1);
|
||||||
|
+ if (!inode_ref)
|
||||||
|
+ return grub_error(GRUB_ERR_OUT_OF_MEMORY,
|
||||||
|
+ "couldn't allocate memory for inode_ref (%d)\n");
|
||||||
|
+
|
||||||
|
+ err = grub_btrfs_read_logical(data, elemaddr, inode_ref, elemsize, 0);
|
||||||
|
+ if (err)
|
||||||
|
+ return grub_error(err, "read_logical caught %d\n", err);
|
||||||
|
+
|
||||||
|
+ alloc += inode_ref->name_len + 2;
|
||||||
|
+ new = grub_malloc(alloc);
|
||||||
|
+ if (!new)
|
||||||
|
+ return grub_error(GRUB_ERR_OUT_OF_MEMORY,
|
||||||
|
+ "couldn't allocate memory for name (%d)\n", alloc);
|
||||||
|
+
|
||||||
|
+ grub_memcpy(new, inode_ref->name, inode_ref->name_len);
|
||||||
|
+ new[inode_ref->name_len] = 0;
|
||||||
|
+ grub_free(inode_ref);
|
||||||
|
+ if (p)
|
||||||
|
+ {
|
||||||
|
+ grub_strcat(new, "/");
|
||||||
|
+ grub_strcat(new, p);
|
||||||
|
+ new[alloc - 1] = 0;
|
||||||
|
+ grub_free(p);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ p = new;
|
||||||
|
+
|
||||||
|
+ key.object_id = key_out.offset;
|
||||||
|
+
|
||||||
|
+ err = lower_bound(data, &key, &key_out, fs_root, &elemaddr,
|
||||||
|
+ &elemsize, &desc, 0);
|
||||||
|
+ if (err)
|
||||||
|
+ return grub_error(err, "lower_bound caught %d\n", err);
|
||||||
|
+
|
||||||
|
+ if (key_out.type != GRUB_BTRFS_ITEM_TYPE_INODE_REF)
|
||||||
|
+ next(data, &desc, &elemaddr, &elemsize, &key_out);
|
||||||
|
+
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ *pathname = p;
|
||||||
|
+ return 0;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static grub_err_t
|
||||||
|
find_path (struct grub_btrfs_data *data,
|
||||||
|
const char *path, struct grub_btrfs_key *key,
|
||||||
|
grub_uint64_t *tree, grub_uint8_t *type)
|
||||||
|
@@ -1222,6 +1400,17 @@ find_path (struct grub_btrfs_data *data,
|
||||||
|
key->offset = 0;
|
||||||
|
follow_default = 1;
|
||||||
|
origpath = grub_strdup (path);
|
||||||
|
+
|
||||||
|
+ if (data->fs_tree)
|
||||||
|
+ {
|
||||||
|
+ *tree = data->fs_tree;
|
||||||
|
+
|
||||||
|
+ follow_default = 0; /* only the toplevel root has a special 'default' */
|
||||||
|
+
|
||||||
|
+ /* This is a tree root, so everything starts at objectid 256 */
|
||||||
|
+ key->object_id = grub_cpu_to_le64_compile_time (GRUB_BTRFS_OBJECT_ID_CHUNK);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if (!origpath)
|
||||||
|
return grub_errno;
|
||||||
|
|
||||||
|
@@ -1657,6 +1846,20 @@ grub_btrfs_read (grub_file_t file, char
|
||||||
|
data->tree, file->offset, buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
+static char *
|
||||||
|
+btrfs_unparse_uuid(struct grub_btrfs_data *data)
|
||||||
|
+{
|
||||||
|
+ return grub_xasprintf ("%04x%04x-%04x-%04x-%04x-%04x%04x%04x",
|
||||||
|
+ grub_be_to_cpu16 (data->sblock.uuid[0]),
|
||||||
|
+ grub_be_to_cpu16 (data->sblock.uuid[1]),
|
||||||
|
+ grub_be_to_cpu16 (data->sblock.uuid[2]),
|
||||||
|
+ grub_be_to_cpu16 (data->sblock.uuid[3]),
|
||||||
|
+ grub_be_to_cpu16 (data->sblock.uuid[4]),
|
||||||
|
+ grub_be_to_cpu16 (data->sblock.uuid[5]),
|
||||||
|
+ grub_be_to_cpu16 (data->sblock.uuid[6]),
|
||||||
|
+ grub_be_to_cpu16 (data->sblock.uuid[7]));
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
static grub_err_t
|
||||||
|
grub_btrfs_uuid (grub_device_t device, char **uuid)
|
||||||
|
{
|
||||||
|
@@ -1668,15 +1871,7 @@ grub_btrfs_uuid (grub_device_t device, c
|
||||||
|
if (!data)
|
||||||
|
return grub_errno;
|
||||||
|
|
||||||
|
- *uuid = grub_xasprintf ("%04x%04x-%04x-%04x-%04x-%04x%04x%04x",
|
||||||
|
- grub_be_to_cpu16 (data->sblock.uuid[0]),
|
||||||
|
- grub_be_to_cpu16 (data->sblock.uuid[1]),
|
||||||
|
- grub_be_to_cpu16 (data->sblock.uuid[2]),
|
||||||
|
- grub_be_to_cpu16 (data->sblock.uuid[3]),
|
||||||
|
- grub_be_to_cpu16 (data->sblock.uuid[4]),
|
||||||
|
- grub_be_to_cpu16 (data->sblock.uuid[5]),
|
||||||
|
- grub_be_to_cpu16 (data->sblock.uuid[6]),
|
||||||
|
- grub_be_to_cpu16 (data->sblock.uuid[7]));
|
||||||
|
+ *uuid = btrfs_unparse_uuid(data);
|
||||||
|
|
||||||
|
grub_btrfs_unmount (data);
|
||||||
|
|
||||||
|
@@ -1733,6 +1928,242 @@ grub_btrfs_embed (grub_device_t device _
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+static grub_err_t
|
||||||
|
+grub_cmd_btrfs_info (grub_command_t cmd __attribute__ ((unused)), int argc,
|
||||||
|
+ char **argv)
|
||||||
|
+{
|
||||||
|
+ grub_device_t dev;
|
||||||
|
+ char *devname;
|
||||||
|
+ struct grub_btrfs_data *data;
|
||||||
|
+ char *uuid;
|
||||||
|
+
|
||||||
|
+ if (argc < 1)
|
||||||
|
+ return grub_error (GRUB_ERR_BAD_ARGUMENT, "device name required");
|
||||||
|
+
|
||||||
|
+ devname = grub_file_get_device_name(argv[0]);
|
||||||
|
+
|
||||||
|
+ if (!devname)
|
||||||
|
+ return grub_errno;
|
||||||
|
+
|
||||||
|
+ dev = grub_device_open (devname);
|
||||||
|
+ grub_free (devname);
|
||||||
|
+ if (!dev)
|
||||||
|
+ return grub_errno;
|
||||||
|
+
|
||||||
|
+ data = grub_btrfs_mount (dev);
|
||||||
|
+ if (!data)
|
||||||
|
+ {
|
||||||
|
+ grub_device_close(dev);
|
||||||
|
+ return grub_error (GRUB_ERR_BAD_ARGUMENT, "failed to open fs");
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (data->sblock.label)
|
||||||
|
+ grub_printf("Label: '%s' ", data->sblock.label);
|
||||||
|
+ else
|
||||||
|
+ grub_printf("Label: none ");
|
||||||
|
+
|
||||||
|
+ uuid = btrfs_unparse_uuid(data);
|
||||||
|
+
|
||||||
|
+ grub_printf(" uuid: %s\n\tTotal devices %" PRIuGRUB_UINT64_T
|
||||||
|
+ " FS bytes used %" PRIuGRUB_UINT64_T "\n",
|
||||||
|
+ uuid, grub_cpu_to_le64(data->sblock.num_devices),
|
||||||
|
+ grub_cpu_to_le64(data->sblock.bytes_used));
|
||||||
|
+
|
||||||
|
+ grub_btrfs_unmount (data);
|
||||||
|
+
|
||||||
|
+ return 0;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static grub_err_t
|
||||||
|
+get_fs_root(struct grub_btrfs_data *data, grub_uint64_t tree,
|
||||||
|
+ grub_uint64_t objectid, grub_uint64_t offset,
|
||||||
|
+ grub_uint64_t *fs_root)
|
||||||
|
+{
|
||||||
|
+ grub_err_t err;
|
||||||
|
+ struct grub_btrfs_key key_in = {
|
||||||
|
+ .object_id = objectid,
|
||||||
|
+ .type = GRUB_BTRFS_ROOT_ITEM_KEY,
|
||||||
|
+ .offset = offset,
|
||||||
|
+ }, key_out;
|
||||||
|
+ struct grub_btrfs_leaf_descriptor desc;
|
||||||
|
+ grub_disk_addr_t elemaddr;
|
||||||
|
+ grub_size_t elemsize;
|
||||||
|
+ struct grub_btrfs_root_item ri;
|
||||||
|
+
|
||||||
|
+ err = lower_bound(data, &key_in, &key_out, tree,
|
||||||
|
+ &elemaddr, &elemsize, &desc, 0);
|
||||||
|
+
|
||||||
|
+ if (err)
|
||||||
|
+ return err;
|
||||||
|
+
|
||||||
|
+ if (key_out.type != GRUB_BTRFS_ITEM_TYPE_ROOT_ITEM || elemaddr == 0)
|
||||||
|
+ return grub_error(GRUB_ERR_FILE_NOT_FOUND,
|
||||||
|
+ N_("can't find fs root for subvol %"PRIuGRUB_UINT64_T"\n"),
|
||||||
|
+ key_in.object_id);
|
||||||
|
+
|
||||||
|
+ err = grub_btrfs_read_logical (data, elemaddr, &ri, sizeof (ri), 0);
|
||||||
|
+ if (err)
|
||||||
|
+ return err;
|
||||||
|
+
|
||||||
|
+ *fs_root = ri.tree;
|
||||||
|
+
|
||||||
|
+ return GRUB_ERR_NONE;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static const struct grub_arg_option options[] = {
|
||||||
|
+ {"output", 'o', 0, N_("Output to a variable instead of the console."),
|
||||||
|
+ N_("VARNAME"), ARG_TYPE_STRING},
|
||||||
|
+ {"path-only", 'p', 0, N_("Show only the path of the subvolume."), 0, 0},
|
||||||
|
+ {"id-only", 'i', 0, N_("Show only the id of the subvolume."), 0, 0},
|
||||||
|
+ {0, 0, 0, 0, 0, 0}
|
||||||
|
+};
|
||||||
|
+
|
||||||
|
+static grub_err_t
|
||||||
|
+grub_cmd_btrfs_list_subvols (struct grub_extcmd_context *ctxt,
|
||||||
|
+ int argc, char **argv)
|
||||||
|
+{
|
||||||
|
+ struct grub_btrfs_data *data;
|
||||||
|
+ grub_device_t dev;
|
||||||
|
+ char *devname;
|
||||||
|
+ grub_uint64_t tree;
|
||||||
|
+ struct grub_btrfs_key key_in = {
|
||||||
|
+ .object_id = grub_cpu_to_le64(GRUB_BTRFS_FS_TREE_OBJECTID),
|
||||||
|
+ .type = GRUB_BTRFS_ROOT_REF_KEY,
|
||||||
|
+ .offset = 0,
|
||||||
|
+ }, key_out;
|
||||||
|
+ struct grub_btrfs_leaf_descriptor desc;
|
||||||
|
+ grub_disk_addr_t elemaddr;
|
||||||
|
+ grub_uint64_t fs_root = 0;
|
||||||
|
+ grub_size_t elemsize;
|
||||||
|
+ grub_size_t allocated = 0;
|
||||||
|
+ int r = 0;
|
||||||
|
+ grub_err_t err;
|
||||||
|
+ char *buf = NULL;
|
||||||
|
+ int print = 1;
|
||||||
|
+ int path_only = ctxt->state[1].set;
|
||||||
|
+ int num_only = ctxt->state[2].set;
|
||||||
|
+ char *varname = NULL;
|
||||||
|
+ char *output = NULL;
|
||||||
|
+
|
||||||
|
+ if (ctxt->state[0].set) {
|
||||||
|
+ varname = ctxt->state[0].arg;
|
||||||
|
+ print = 0;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (argc < 1)
|
||||||
|
+ return grub_error (GRUB_ERR_BAD_ARGUMENT, "device name required");
|
||||||
|
+
|
||||||
|
+ devname = grub_file_get_device_name(argv[0]);
|
||||||
|
+ if (!devname)
|
||||||
|
+ return grub_errno;
|
||||||
|
+
|
||||||
|
+ dev = grub_device_open (devname);
|
||||||
|
+ grub_free (devname);
|
||||||
|
+ if (!dev)
|
||||||
|
+ return grub_errno;
|
||||||
|
+
|
||||||
|
+ data = grub_btrfs_mount(dev);
|
||||||
|
+ if (!data)
|
||||||
|
+ return grub_error (GRUB_ERR_BAD_ARGUMENT, "could not open device");
|
||||||
|
+
|
||||||
|
+ tree = grub_le_to_cpu64(data->sblock.root_tree);
|
||||||
|
+ err = get_fs_root(data, tree, grub_cpu_to_le64(GRUB_BTRFS_FS_TREE_OBJECTID),
|
||||||
|
+ 0, &fs_root);
|
||||||
|
+ if (err)
|
||||||
|
+ goto out;
|
||||||
|
+
|
||||||
|
+ err = lower_bound(data, &key_in, &key_out, tree,
|
||||||
|
+ &elemaddr, &elemsize, &desc, 0);
|
||||||
|
+
|
||||||
|
+ if (err)
|
||||||
|
+ {
|
||||||
|
+ grub_btrfs_unmount(data);
|
||||||
|
+ return err;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (key_out.type != GRUB_BTRFS_ITEM_TYPE_ROOT_REF || elemaddr == 0)
|
||||||
|
+ {
|
||||||
|
+ r = next(data, &desc, &elemaddr, &elemsize, &key_out);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (key_out.type != GRUB_BTRFS_ITEM_TYPE_ROOT_REF) {
|
||||||
|
+ err = GRUB_ERR_FILE_NOT_FOUND;
|
||||||
|
+ grub_error(GRUB_ERR_FILE_NOT_FOUND, N_("can't find root refs"));
|
||||||
|
+ goto out;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ do
|
||||||
|
+ {
|
||||||
|
+ struct grub_btrfs_root_ref *ref;
|
||||||
|
+ char *p = NULL;
|
||||||
|
+
|
||||||
|
+ if (key_out.type != GRUB_BTRFS_ITEM_TYPE_ROOT_REF)
|
||||||
|
+ {
|
||||||
|
+ r = 0;
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (elemsize > allocated)
|
||||||
|
+ {
|
||||||
|
+ grub_free(buf);
|
||||||
|
+ allocated = 2 * elemsize;
|
||||||
|
+ buf = grub_malloc(allocated + 1);
|
||||||
|
+ if (!buf)
|
||||||
|
+ {
|
||||||
|
+ r = -grub_errno;
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ ref = (struct grub_btrfs_root_ref *)buf;
|
||||||
|
+
|
||||||
|
+ err = grub_btrfs_read_logical(data, elemaddr, buf, elemsize, 0);
|
||||||
|
+ if (err)
|
||||||
|
+ {
|
||||||
|
+ r = -err;
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
+ buf[elemsize] = 0;
|
||||||
|
+
|
||||||
|
+ find_pathname(data, ref->dirid, fs_root, ref->name, &p);
|
||||||
|
+
|
||||||
|
+ if (print)
|
||||||
|
+ {
|
||||||
|
+ if (num_only)
|
||||||
|
+ grub_printf("ID %"PRIuGRUB_UINT64_T"\n", key_out.offset);
|
||||||
|
+ else if (path_only)
|
||||||
|
+ grub_printf("%s\n", p);
|
||||||
|
+ else
|
||||||
|
+ grub_printf("ID %"PRIuGRUB_UINT64_T" path %s\n", key_out.offset, p);
|
||||||
|
+ } else {
|
||||||
|
+ char *old = output;
|
||||||
|
+ if (num_only)
|
||||||
|
+ output = grub_xasprintf("%s%"PRIuGRUB_UINT64_T"\n",
|
||||||
|
+ old ?: "", key_out.offset);
|
||||||
|
+ else if (path_only)
|
||||||
|
+ output = grub_xasprintf("%s%s\n", old ?: "", p);
|
||||||
|
+ else
|
||||||
|
+ output = grub_xasprintf("%sID %"PRIuGRUB_UINT64_T" path %s\n",
|
||||||
|
+ old ?: "", key_out.offset, p);
|
||||||
|
+
|
||||||
|
+ if (old)
|
||||||
|
+ grub_free(old);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ r = next(data, &desc, &elemaddr, &elemsize, &key_out);
|
||||||
|
+ } while(r > 0);
|
||||||
|
+
|
||||||
|
+ if (output)
|
||||||
|
+ grub_env_set(varname, output);
|
||||||
|
+
|
||||||
|
+out:
|
||||||
|
+ free_iterator(&desc);
|
||||||
|
+ grub_btrfs_unmount(data);
|
||||||
|
+
|
||||||
|
+ grub_device_close (dev);
|
||||||
|
+
|
||||||
|
+ return 0;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
static struct grub_fs grub_btrfs_fs = {
|
||||||
|
.name = "btrfs",
|
||||||
|
.dir = grub_btrfs_dir,
|
||||||
|
@@ -1748,12 +2179,88 @@ static struct grub_fs grub_btrfs_fs = {
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
+static grub_command_t cmd_info;
|
||||||
|
+static grub_extcmd_t cmd_list_subvols;
|
||||||
|
+
|
||||||
|
+static char *
|
||||||
|
+subvolid_set_env (struct grub_env_var *var __attribute__ ((unused)),
|
||||||
|
+ const char *val)
|
||||||
|
+{
|
||||||
|
+ unsigned long long result = 0;
|
||||||
|
+
|
||||||
|
+ grub_errno = GRUB_ERR_NONE;
|
||||||
|
+ if (*val)
|
||||||
|
+ {
|
||||||
|
+ result = grub_strtoull(val, NULL, 10);
|
||||||
|
+ if (grub_errno)
|
||||||
|
+ return NULL;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ grub_free (btrfs_default_subvol);
|
||||||
|
+ btrfs_default_subvol = NULL;
|
||||||
|
+ btrfs_default_subvolid = result;
|
||||||
|
+ return grub_strdup(val);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static const char *
|
||||||
|
+subvolid_get_env (struct grub_env_var *var __attribute__ ((unused)),
|
||||||
|
+ const char *val __attribute__ ((unused)))
|
||||||
|
+{
|
||||||
|
+ if (btrfs_default_subvol)
|
||||||
|
+ return grub_xasprintf("subvol:%s", btrfs_default_subvol);
|
||||||
|
+ else if (btrfs_default_subvolid)
|
||||||
|
+ return grub_xasprintf("%"PRIuGRUB_UINT64_T, btrfs_default_subvolid);
|
||||||
|
+ else
|
||||||
|
+ return "";
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static char *
|
||||||
|
+subvol_set_env (struct grub_env_var *var __attribute__ ((unused)),
|
||||||
|
+ const char *val)
|
||||||
|
+{
|
||||||
|
+ grub_free (btrfs_default_subvol);
|
||||||
|
+ btrfs_default_subvol = grub_strdup (val);
|
||||||
|
+ btrfs_default_subvolid = 0;
|
||||||
|
+ return grub_strdup(val);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+static const char *
|
||||||
|
+subvol_get_env (struct grub_env_var *var __attribute__ ((unused)),
|
||||||
|
+ const char *val __attribute__ ((unused)))
|
||||||
|
+{
|
||||||
|
+ if (btrfs_default_subvol)
|
||||||
|
+ return btrfs_default_subvol;
|
||||||
|
+ else if (btrfs_default_subvolid)
|
||||||
|
+ return grub_xasprintf("subvolid:%" PRIuGRUB_UINT64_T,
|
||||||
|
+ btrfs_default_subvolid);
|
||||||
|
+ else
|
||||||
|
+ return "";
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
GRUB_MOD_INIT (btrfs)
|
||||||
|
{
|
||||||
|
grub_fs_register (&grub_btrfs_fs);
|
||||||
|
+ cmd_info = grub_register_command("btrfs-info", grub_cmd_btrfs_info,
|
||||||
|
+ "DEVICE",
|
||||||
|
+ "Print BtrFS info about DEVICE.");
|
||||||
|
+ cmd_list_subvols = grub_register_extcmd("btrfs-list-subvols",
|
||||||
|
+ grub_cmd_btrfs_list_subvols, 0,
|
||||||
|
+ "[-p|-n] [-o var] DEVICE",
|
||||||
|
+ "Print list of BtrFS subvolumes on "
|
||||||
|
+ "DEVICE.", options);
|
||||||
|
+ grub_register_variable_hook ("btrfs_subvol", subvol_get_env,
|
||||||
|
+ subvol_set_env);
|
||||||
|
+ grub_register_variable_hook ("btrfs_subvolid", subvolid_get_env,
|
||||||
|
+ subvolid_set_env);
|
||||||
|
}
|
||||||
|
|
||||||
|
GRUB_MOD_FINI (btrfs)
|
||||||
|
{
|
||||||
|
+ grub_register_variable_hook ("btrfs_subvol", NULL, NULL);
|
||||||
|
+ grub_register_variable_hook ("btrfs_subvolid", NULL, NULL);
|
||||||
|
+ grub_unregister_command (cmd_info);
|
||||||
|
+ grub_unregister_extcmd (cmd_list_subvols);
|
||||||
|
grub_fs_unregister (&grub_btrfs_fs);
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+// vim: si et sw=2:
|
66
0003-cmdline-add-envvar-loader_cmdline_append.patch
Normal file
66
0003-cmdline-add-envvar-loader_cmdline_append.patch
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
From: Michael Chang <mchang@suse.com>
|
||||||
|
Subject: add loader_cmdline_append environment variable
|
||||||
|
|
||||||
|
Add loader_cmdline_append environment variable that appends it's
|
||||||
|
value to the loader's command line. We can use this variable to
|
||||||
|
assign values determined at run time. It will take effect on any
|
||||||
|
subsidiary configs loaded using configfile as well.
|
||||||
|
|
||||||
|
By means of this variable, we can, for example, set rootflags=
|
||||||
|
according to the selected btrfs snapshots and tell linux kernel's
|
||||||
|
btrfs module to mount the snapshot by the subvolume name or id.
|
||||||
|
|
||||||
|
Signed-off-by: Michael Chang <mchang@suse.com>
|
||||||
|
---
|
||||||
|
grub-core/lib/cmdline.c | 21 +++++++++++++++++++++
|
||||||
|
1 file changed, 21 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/grub-core/lib/cmdline.c b/grub-core/lib/cmdline.c
|
||||||
|
index a702e64..c5be945 100644
|
||||||
|
--- a/grub-core/lib/cmdline.c
|
||||||
|
+++ b/grub-core/lib/cmdline.c
|
||||||
|
@@ -19,6 +19,8 @@
|
||||||
|
|
||||||
|
#include <grub/lib/cmdline.h>
|
||||||
|
#include <grub/misc.h>
|
||||||
|
+#include <grub/mm.h>
|
||||||
|
+#include <grub/env.h>
|
||||||
|
|
||||||
|
static unsigned int check_arg (char *c, int *has_space)
|
||||||
|
{
|
||||||
|
@@ -65,6 +67,8 @@ int grub_create_loader_cmdline (int argc, char *argv[], char *buf,
|
||||||
|
int i, space;
|
||||||
|
unsigned int arg_size;
|
||||||
|
char *c;
|
||||||
|
+ const char *append = NULL;
|
||||||
|
+ grub_size_t append_size = 0;
|
||||||
|
|
||||||
|
for (i = 0; i < argc; i++)
|
||||||
|
{
|
||||||
|
@@ -95,6 +99,23 @@ int grub_create_loader_cmdline (int argc, char *argv[], char *buf,
|
||||||
|
*buf++ = ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
+ append = grub_env_get ("loader_cmdline_append");
|
||||||
|
+
|
||||||
|
+ if (append)
|
||||||
|
+ append_size = grub_strlen (append);
|
||||||
|
+
|
||||||
|
+ if (append_size)
|
||||||
|
+ {
|
||||||
|
+ append_size++;
|
||||||
|
+ if (size >= append_size)
|
||||||
|
+ {
|
||||||
|
+ grub_strcpy (buf, append);
|
||||||
|
+ buf += append_size;
|
||||||
|
+ size -= append_size;
|
||||||
|
+ i++;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
/* Replace last space with null. */
|
||||||
|
if (i)
|
||||||
|
buf--;
|
||||||
|
--
|
||||||
|
1.8.1.4
|
||||||
|
|
21
0004-btrfs-export-subvolume-envvars.patch
Normal file
21
0004-btrfs-export-subvolume-envvars.patch
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
From: Michael Chang <mchang@suse.com>
|
||||||
|
Subject: export btrfs_subvol and btrfs_subvolid
|
||||||
|
|
||||||
|
We should export btrfs_subvol and btrfs_subvolid to have both visible
|
||||||
|
to subsidiary configuration files loaded using configfile.
|
||||||
|
|
||||||
|
Signed-off-by: Michael Chang <mchang@suse.com>
|
||||||
|
|
||||||
|
Index: grub-2.00/grub-core/fs/btrfs.c
|
||||||
|
===================================================================
|
||||||
|
--- grub-2.00.orig/grub-core/fs/btrfs.c
|
||||||
|
+++ grub-2.00/grub-core/fs/btrfs.c
|
||||||
|
@@ -2252,6 +2252,8 @@ GRUB_MOD_INIT (btrfs)
|
||||||
|
subvol_set_env);
|
||||||
|
grub_register_variable_hook ("btrfs_subvolid", subvolid_get_env,
|
||||||
|
subvolid_set_env);
|
||||||
|
+ grub_env_export ("btrfs_subvol");
|
||||||
|
+ grub_env_export ("btrfs_subvolid");
|
||||||
|
}
|
||||||
|
|
||||||
|
GRUB_MOD_FINI (btrfs)
|
@ -1,3 +1,12 @@
|
|||||||
|
-------------------------------------------------------------------
|
||||||
|
Tue Dec 17 07:20:33 UTC 2013 - mchang@suse.com
|
||||||
|
|
||||||
|
- add new patches for booting btrfs snapshot (fate#316522) (fate#316232)
|
||||||
|
* 0001-btrfs-rename-skip_default-to-follow_default.patch
|
||||||
|
* 0002-btrfs-add-ability-to-boot-from-subvolumes.patch
|
||||||
|
* 0003-cmdline-add-envvar-loader_cmdline_append.patch
|
||||||
|
* 0004-btrfs-export-subvolume-envvars.patch
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Tue Dec 10 19:13:53 UTC 2013 - arvidjaar@gmail.com
|
Tue Dec 10 19:13:53 UTC 2013 - arvidjaar@gmail.com
|
||||||
|
|
||||||
|
@ -135,6 +135,11 @@ Patch37: grub2-fix-descriptor-leak-in-grub_util_is_imsm.patch
|
|||||||
Patch38: grub2-fix-x86_64-efi-startup-stack-alignment.patch
|
Patch38: grub2-fix-x86_64-efi-startup-stack-alignment.patch
|
||||||
Patch39: grub2-fix-x86_64-efi-callwrap-stack-alignment.patch
|
Patch39: grub2-fix-x86_64-efi-callwrap-stack-alignment.patch
|
||||||
Patch40: 0001-Fix-build-with-FreeType-2.5.1.patch
|
Patch40: 0001-Fix-build-with-FreeType-2.5.1.patch
|
||||||
|
# Btrfs snapshot booting related patches
|
||||||
|
Patch100: 0001-btrfs-rename-skip_default-to-follow_default.patch
|
||||||
|
Patch101: 0002-btrfs-add-ability-to-boot-from-subvolumes.patch
|
||||||
|
Patch102: 0003-cmdline-add-envvar-loader_cmdline_append.patch
|
||||||
|
Patch103: 0004-btrfs-export-subvolume-envvars.patch
|
||||||
Requires: gettext-runtime
|
Requires: gettext-runtime
|
||||||
%if 0%{?suse_version} >= 1140
|
%if 0%{?suse_version} >= 1140
|
||||||
Requires: os-prober
|
Requires: os-prober
|
||||||
@ -254,6 +259,10 @@ mv po/grub.pot po/%{name}.pot
|
|||||||
%patch38 -p1
|
%patch38 -p1
|
||||||
%patch39 -p1
|
%patch39 -p1
|
||||||
%patch40 -p1
|
%patch40 -p1
|
||||||
|
%patch100 -p1
|
||||||
|
%patch101 -p1
|
||||||
|
%patch102 -p1
|
||||||
|
%patch103 -p1
|
||||||
|
|
||||||
# Generate po/LINGUAS for message catalogs ...
|
# Generate po/LINGUAS for message catalogs ...
|
||||||
./linguas.sh
|
./linguas.sh
|
||||||
|
Loading…
Reference in New Issue
Block a user