1#!/usr/bin/env python3
2#
3# Copyright (c) 2013, Intel Corporation.
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7# DESCRIPTION 'wic' is the OpenEmbedded Image Creator that users can
8# use to generate bootable images.  Invoking it without any arguments
9# will display help screens for the 'wic' command and list the
10# available 'wic' subcommands.  Invoking a subcommand without any
11# arguments will likewise display help screens for the specified
12# subcommand.  Please use that interface for detailed help.
13#
14# AUTHORS
15# Tom Zanussi <tom.zanussi (at] linux.intel.com>
16#
17__version__ = "0.2.0"
18
19# Python Standard Library modules
20import os
21import sys
22import argparse
23import logging
24import subprocess
25import shutil
26
27from collections import namedtuple
28
29# External modules
30scripts_path = os.path.dirname(os.path.realpath(__file__))
31lib_path = scripts_path + '/lib'
32sys.path.insert(0, lib_path)
33import scriptpath
34scriptpath.add_oe_lib_path()
35
36# Check whether wic is running within eSDK environment
37sdkroot = scripts_path
38if os.environ.get('SDKTARGETSYSROOT'):
39    while sdkroot != '' and sdkroot != os.sep:
40        if os.path.exists(os.path.join(sdkroot, '.devtoolbase')):
41            # Set BUILDDIR for wic to work within eSDK
42            os.environ['BUILDDIR'] = sdkroot
43            # .devtoolbase only exists within eSDK
44            # If found, initialize bitbake path for eSDK environment and append to PATH
45            sdkroot = os.path.join(os.path.dirname(scripts_path), 'bitbake', 'bin')
46            os.environ['PATH'] += ":" + sdkroot
47            break
48        sdkroot = os.path.dirname(sdkroot)
49
50bitbake_exe = shutil.which('bitbake')
51if bitbake_exe:
52    bitbake_path = scriptpath.add_bitbake_lib_path()
53    import bb
54
55from wic import WicError
56from wic.misc import get_bitbake_var, BB_VARS
57from wic import engine
58from wic import help as hlp
59
60
61def wic_logger():
62    """Create and convfigure wic logger."""
63    logger = logging.getLogger('wic')
64    logger.setLevel(logging.INFO)
65
66    handler = logging.StreamHandler()
67
68    formatter = logging.Formatter('%(levelname)s: %(message)s')
69    handler.setFormatter(formatter)
70
71    logger.addHandler(handler)
72
73    return logger
74
75logger = wic_logger()
76
77def rootfs_dir_to_args(krootfs_dir):
78    """
79    Get a rootfs_dir dict and serialize to string
80    """
81    rootfs_dir = ''
82    for key, val in krootfs_dir.items():
83        rootfs_dir += ' '
84        rootfs_dir += '='.join([key, val])
85    return rootfs_dir.strip()
86
87
88class RootfsArgAction(argparse.Action):
89    def __init__(self, **kwargs):
90        super().__init__(**kwargs)
91
92    def __call__(self, parser, namespace, value, option_string=None):
93        if not "rootfs_dir" in vars(namespace) or \
94           not type(namespace.__dict__['rootfs_dir']) is dict:
95            namespace.__dict__['rootfs_dir'] = {}
96
97        if '=' in value:
98            (key, rootfs_dir) = value.split('=')
99        else:
100            key = 'ROOTFS_DIR'
101            rootfs_dir = value
102
103        namespace.__dict__['rootfs_dir'][key] = rootfs_dir
104
105
106def wic_create_subcommand(options, usage_str):
107    """
108    Command-line handling for image creation.  The real work is done
109    by image.engine.wic_create()
110    """
111    if options.build_rootfs and not bitbake_exe:
112        raise WicError("Can't build rootfs as bitbake is not in the $PATH")
113
114    if not options.image_name:
115        missed = []
116        for val, opt in [(options.rootfs_dir, 'rootfs-dir'),
117                         (options.bootimg_dir, 'bootimg-dir'),
118                         (options.kernel_dir, 'kernel-dir'),
119                         (options.native_sysroot, 'native-sysroot')]:
120            if not val:
121                missed.append(opt)
122        if missed:
123            raise WicError("The following build artifacts are not specified: %s" %
124                           ", ".join(missed))
125
126    if options.image_name:
127        BB_VARS.default_image = options.image_name
128    else:
129        options.build_check = False
130
131    if options.vars_dir:
132        BB_VARS.vars_dir = options.vars_dir
133
134    if options.build_check and not engine.verify_build_env():
135        raise WicError("Couldn't verify build environment, exiting")
136
137    if options.debug:
138        logger.setLevel(logging.DEBUG)
139
140    if options.image_name:
141        if options.build_rootfs:
142            argv = ["bitbake", options.image_name]
143            if options.debug:
144                argv.append("--debug")
145
146            logger.info("Building rootfs...\n")
147            subprocess.check_call(argv)
148
149        rootfs_dir = get_bitbake_var("IMAGE_ROOTFS", options.image_name)
150        kernel_dir = get_bitbake_var("DEPLOY_DIR_IMAGE", options.image_name)
151        bootimg_dir = get_bitbake_var("STAGING_DATADIR", options.image_name)
152
153        native_sysroot = options.native_sysroot
154        if options.vars_dir and not native_sysroot:
155            native_sysroot = get_bitbake_var("RECIPE_SYSROOT_NATIVE", options.image_name)
156    else:
157        if options.build_rootfs:
158            raise WicError("Image name is not specified, exiting. "
159                           "(Use -e/--image-name to specify it)")
160        native_sysroot = options.native_sysroot
161
162    if options.kernel_dir:
163        kernel_dir = options.kernel_dir
164
165    if not options.vars_dir and (not native_sysroot or not os.path.isdir(native_sysroot)):
166        logger.info("Building wic-tools...\n")
167        subprocess.check_call(["bitbake", "wic-tools"])
168        native_sysroot = get_bitbake_var("RECIPE_SYSROOT_NATIVE", "wic-tools")
169
170    if not native_sysroot:
171        raise WicError("Unable to find the location of the native tools sysroot")
172
173    wks_file = options.wks_file
174
175    if not wks_file.endswith(".wks"):
176        wks_file = engine.find_canned_image(scripts_path, wks_file)
177        if not wks_file:
178            raise WicError("No image named %s found, exiting.  (Use 'wic list images' "
179                           "to list available images, or specify a fully-qualified OE "
180                           "kickstart (.wks) filename)" % options.wks_file)
181
182    if not options.image_name:
183        rootfs_dir = ''
184        if 'ROOTFS_DIR' in options.rootfs_dir:
185            rootfs_dir = options.rootfs_dir['ROOTFS_DIR']
186        bootimg_dir = options.bootimg_dir
187        kernel_dir = options.kernel_dir
188        native_sysroot = options.native_sysroot
189        if rootfs_dir and not os.path.isdir(rootfs_dir):
190            raise WicError("--rootfs-dir (-r) not found, exiting")
191        if not os.path.isdir(bootimg_dir):
192            raise WicError("--bootimg-dir (-b) not found, exiting")
193        if not os.path.isdir(kernel_dir):
194            raise WicError("--kernel-dir (-k) not found, exiting")
195        if not os.path.isdir(native_sysroot):
196            raise WicError("--native-sysroot (-n) not found, exiting")
197    else:
198        not_found = not_found_dir = ""
199        if not os.path.isdir(rootfs_dir):
200            (not_found, not_found_dir) = ("rootfs-dir", rootfs_dir)
201        elif not os.path.isdir(kernel_dir):
202            (not_found, not_found_dir) = ("kernel-dir", kernel_dir)
203        elif not os.path.isdir(native_sysroot):
204            (not_found, not_found_dir) = ("native-sysroot", native_sysroot)
205        if not_found:
206            if not not_found_dir:
207                not_found_dir = "Completely missing artifact - wrong image (.wks) used?"
208            logger.info("Build artifacts not found, exiting.")
209            logger.info("  (Please check that the build artifacts for the machine")
210            logger.info("   selected in local.conf actually exist and that they")
211            logger.info("   are the correct artifacts for the image (.wks file)).\n")
212            raise WicError("The artifact that couldn't be found was %s:\n  %s" % (not_found, not_found_dir))
213
214    krootfs_dir = options.rootfs_dir
215    if krootfs_dir is None:
216        krootfs_dir = {}
217        krootfs_dir['ROOTFS_DIR'] = rootfs_dir
218
219    rootfs_dir = rootfs_dir_to_args(krootfs_dir)
220
221    logger.info("Creating image(s)...\n")
222    engine.wic_create(wks_file, rootfs_dir, bootimg_dir, kernel_dir,
223                      native_sysroot, options)
224
225
226def wic_list_subcommand(args, usage_str):
227    """
228    Command-line handling for listing available images.
229    The real work is done by image.engine.wic_list()
230    """
231    if not engine.wic_list(args, scripts_path):
232        raise WicError("Bad list arguments, exiting")
233
234
235def wic_ls_subcommand(args, usage_str):
236    """
237    Command-line handling for list content of images.
238    The real work is done by engine.wic_ls()
239    """
240    engine.wic_ls(args, args.native_sysroot)
241
242def wic_cp_subcommand(args, usage_str):
243    """
244    Command-line handling for copying files/dirs to images.
245    The real work is done by engine.wic_cp()
246    """
247    engine.wic_cp(args, args.native_sysroot)
248
249def wic_rm_subcommand(args, usage_str):
250    """
251    Command-line handling for removing files/dirs from images.
252    The real work is done by engine.wic_rm()
253    """
254    engine.wic_rm(args, args.native_sysroot)
255
256def wic_write_subcommand(args, usage_str):
257    """
258    Command-line handling for writing images.
259    The real work is done by engine.wic_write()
260    """
261    engine.wic_write(args, args.native_sysroot)
262
263def wic_help_subcommand(args, usage_str):
264    """
265    Command-line handling for help subcommand to keep the current
266    structure of the function definitions.
267    """
268    pass
269
270
271def wic_help_topic_subcommand(usage_str, help_str):
272    """
273    Display function for help 'sub-subcommands'.
274    """
275    print(help_str)
276    return
277
278
279wic_help_topic_usage = """
280"""
281
282helptopics = {
283    "plugins":   [wic_help_topic_subcommand,
284                  wic_help_topic_usage,
285                  hlp.wic_plugins_help],
286    "overview":  [wic_help_topic_subcommand,
287                  wic_help_topic_usage,
288                  hlp.wic_overview_help],
289    "kickstart": [wic_help_topic_subcommand,
290                  wic_help_topic_usage,
291                  hlp.wic_kickstart_help],
292    "create":    [wic_help_topic_subcommand,
293                  wic_help_topic_usage,
294                  hlp.wic_create_help],
295    "ls":        [wic_help_topic_subcommand,
296                  wic_help_topic_usage,
297                  hlp.wic_ls_help],
298    "cp":        [wic_help_topic_subcommand,
299                  wic_help_topic_usage,
300                  hlp.wic_cp_help],
301    "rm":        [wic_help_topic_subcommand,
302                  wic_help_topic_usage,
303                  hlp.wic_rm_help],
304    "write":     [wic_help_topic_subcommand,
305                  wic_help_topic_usage,
306                  hlp.wic_write_help],
307    "list":      [wic_help_topic_subcommand,
308                  wic_help_topic_usage,
309                  hlp.wic_list_help]
310}
311
312
313def wic_init_parser_create(subparser):
314    subparser.add_argument("wks_file")
315
316    subparser.add_argument("-o", "--outdir", dest="outdir", default='.',
317                      help="name of directory to create image in")
318    subparser.add_argument("-w", "--workdir",
319                      help="temporary workdir to use for intermediate files")
320    subparser.add_argument("-e", "--image-name", dest="image_name",
321                      help="name of the image to use the artifacts from "
322                           "e.g. core-image-sato")
323    subparser.add_argument("-r", "--rootfs-dir", action=RootfsArgAction,
324                      help="path to the /rootfs dir to use as the "
325                           ".wks rootfs source")
326    subparser.add_argument("-b", "--bootimg-dir", dest="bootimg_dir",
327                      help="path to the dir containing the boot artifacts "
328                           "(e.g. /EFI or /syslinux dirs) to use as the "
329                           ".wks bootimg source")
330    subparser.add_argument("-k", "--kernel-dir", dest="kernel_dir",
331                      help="path to the dir containing the kernel to use "
332                           "in the .wks bootimg")
333    subparser.add_argument("-n", "--native-sysroot", dest="native_sysroot",
334                      help="path to the native sysroot containing the tools "
335                           "to use to build the image")
336    subparser.add_argument("-s", "--skip-build-check", dest="build_check",
337                      action="store_false", default=True, help="skip the build check")
338    subparser.add_argument("-f", "--build-rootfs", action="store_true", help="build rootfs")
339    subparser.add_argument("-c", "--compress-with", choices=("gzip", "bzip2", "xz"),
340                      dest='compressor',
341                      help="compress image with specified compressor")
342    subparser.add_argument("-m", "--bmap", action="store_true", help="generate .bmap")
343    subparser.add_argument("--no-fstab-update" ,action="store_true",
344                      help="Do not change fstab file.")
345    subparser.add_argument("-v", "--vars", dest='vars_dir',
346                      help="directory with <image>.env files that store "
347                           "bitbake variables")
348    subparser.add_argument("-D", "--debug", dest="debug", action="store_true",
349                      default=False, help="output debug information")
350    subparser.add_argument("-i", "--imager", dest="imager",
351                      default="direct", help="the wic imager plugin")
352    subparser.add_argument("--extra-space", type=int, dest="extra_space",
353                      default=0, help="additional free disk space to add to the image")
354    return
355
356
357def wic_init_parser_list(subparser):
358    subparser.add_argument("list_type",
359                        help="can be 'images' or 'source-plugins' "
360                             "to obtain a list. "
361                             "If value is a valid .wks image file")
362    subparser.add_argument("help_for", default=[], nargs='*',
363                        help="If 'list_type' is a valid .wks image file "
364                             "this value can be 'help' to show the help information "
365                             "defined inside the .wks file")
366    return
367
368def imgtype(arg):
369    """
370    Custom type for ArgumentParser
371    Converts path spec to named tuple: (image, partition, path)
372    """
373    image = arg
374    part = path = None
375    if ':' in image:
376        image, part = image.split(':')
377        if '/' in part:
378            part, path = part.split('/', 1)
379        if not path:
380            path = '/'
381
382    if not os.path.isfile(image):
383        err = "%s is not a regular file or symlink" % image
384        raise argparse.ArgumentTypeError(err)
385
386    return namedtuple('ImgType', 'image part path')(image, part, path)
387
388def wic_init_parser_ls(subparser):
389    subparser.add_argument("path", type=imgtype,
390                        help="image spec: <image>[:<vfat partition>[<path>]]")
391    subparser.add_argument("-n", "--native-sysroot",
392                        help="path to the native sysroot containing the tools")
393
394def imgpathtype(arg):
395    img = imgtype(arg)
396    if img.part is None:
397        raise argparse.ArgumentTypeError("partition number is not specified")
398    return img
399
400def wic_init_parser_cp(subparser):
401    subparser.add_argument("src",
402                        help="image spec: <image>:<vfat partition>[<path>] or <file>")
403    subparser.add_argument("dest",
404                        help="image spec: <image>:<vfat partition>[<path>] or <file>")
405    subparser.add_argument("-n", "--native-sysroot",
406                        help="path to the native sysroot containing the tools")
407
408def wic_init_parser_rm(subparser):
409    subparser.add_argument("path", type=imgpathtype,
410                        help="path: <image>:<vfat partition><path>")
411    subparser.add_argument("-n", "--native-sysroot",
412                        help="path to the native sysroot containing the tools")
413    subparser.add_argument("-r", dest="recursive_delete", action="store_true", default=False,
414                        help="remove directories and their contents recursively, "
415                        " this only applies to ext* partition")
416
417def expandtype(rules):
418    """
419    Custom type for ArgumentParser
420    Converts expand rules to the dictionary {<partition>: size}
421    """
422    if rules == 'auto':
423        return {}
424    result = {}
425    for rule in rules.split(','):
426        try:
427            part, size = rule.split(':')
428        except ValueError:
429            raise argparse.ArgumentTypeError("Incorrect rule format: %s" % rule)
430
431        if not part.isdigit():
432            raise argparse.ArgumentTypeError("Rule '%s': partition number must be integer" % rule)
433
434        # validate size
435        multiplier = 1
436        for suffix, mult in [('K', 1024), ('M', 1024 * 1024), ('G', 1024 * 1024 * 1024)]:
437            if size.upper().endswith(suffix):
438                multiplier = mult
439                size = size[:-1]
440                break
441        if not size.isdigit():
442            raise argparse.ArgumentTypeError("Rule '%s': size must be integer" % rule)
443
444        result[int(part)] = int(size) * multiplier
445
446    return result
447
448def wic_init_parser_write(subparser):
449    subparser.add_argument("image",
450                        help="path to the wic image")
451    subparser.add_argument("target",
452                        help="target file or device")
453    subparser.add_argument("-e", "--expand", type=expandtype,
454                        help="expand rules: auto or <partition>:<size>[,<partition>:<size>]")
455    subparser.add_argument("-n", "--native-sysroot",
456                        help="path to the native sysroot containing the tools")
457
458def wic_init_parser_help(subparser):
459    helpparsers = subparser.add_subparsers(dest='help_topic', help=hlp.wic_usage)
460    for helptopic in helptopics:
461        helpparsers.add_parser(helptopic, help=helptopics[helptopic][2])
462    return
463
464
465subcommands = {
466    "create":    [wic_create_subcommand,
467                  hlp.wic_create_usage,
468                  hlp.wic_create_help,
469                  wic_init_parser_create],
470    "list":      [wic_list_subcommand,
471                  hlp.wic_list_usage,
472                  hlp.wic_list_help,
473                  wic_init_parser_list],
474    "ls":        [wic_ls_subcommand,
475                  hlp.wic_ls_usage,
476                  hlp.wic_ls_help,
477                  wic_init_parser_ls],
478    "cp":        [wic_cp_subcommand,
479                  hlp.wic_cp_usage,
480                  hlp.wic_cp_help,
481                  wic_init_parser_cp],
482    "rm":        [wic_rm_subcommand,
483                  hlp.wic_rm_usage,
484                  hlp.wic_rm_help,
485                  wic_init_parser_rm],
486    "write":     [wic_write_subcommand,
487                  hlp.wic_write_usage,
488                  hlp.wic_write_help,
489                  wic_init_parser_write],
490    "help":      [wic_help_subcommand,
491                  wic_help_topic_usage,
492                  hlp.wic_help_help,
493                  wic_init_parser_help]
494}
495
496
497def init_parser(parser):
498    parser.add_argument("--version", action="version",
499        version="%(prog)s {version}".format(version=__version__))
500    parser.add_argument("-D", "--debug", dest="debug", action="store_true",
501        default=False, help="output debug information")
502
503    subparsers = parser.add_subparsers(dest='command', help=hlp.wic_usage)
504    for subcmd in subcommands:
505        subparser = subparsers.add_parser(subcmd, help=subcommands[subcmd][2])
506        subcommands[subcmd][3](subparser)
507
508class WicArgumentParser(argparse.ArgumentParser):
509     def format_help(self):
510         return hlp.wic_help
511
512def main(argv):
513    parser = WicArgumentParser(
514        description="wic version %s" % __version__)
515
516    init_parser(parser)
517
518    args = parser.parse_args(argv)
519
520    if args.debug:
521        logger.setLevel(logging.DEBUG)
522
523    if "command" in vars(args):
524        if args.command == "help":
525            if args.help_topic is None:
526                parser.print_help()
527            elif args.help_topic in helptopics:
528                hlpt = helptopics[args.help_topic]
529                hlpt[0](hlpt[1], hlpt[2])
530            return 0
531
532    # validate wic cp src and dest parameter to identify which one of it is
533    # image and cast it into imgtype
534    if args.command == "cp":
535        if ":" in args.dest:
536            args.dest = imgtype(args.dest)
537        elif ":" in args.src:
538            args.src = imgtype(args.src)
539        else:
540            raise argparse.ArgumentTypeError("no image or partition number specified.")
541
542    return hlp.invoke_subcommand(args, parser, hlp.wic_help_usage, subcommands)
543
544
545if __name__ == "__main__":
546    try:
547        sys.exit(main(sys.argv[1:]))
548    except WicError as err:
549        print()
550        logger.error(err)
551        sys.exit(1)
552