1# Development tool - import command plugin
2#
3# Copyright (C) 2014-2017 Intel Corporation
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7"""Devtool import plugin"""
8
9import os
10import tarfile
11import logging
12import collections
13import json
14import fnmatch
15
16from devtool import standard, setup_tinfoil, replace_from_file, DevtoolError
17from devtool import export
18
19logger = logging.getLogger('devtool')
20
21def devimport(args, config, basepath, workspace):
22    """Entry point for the devtool 'import' subcommand"""
23
24    def get_pn(name):
25        """ Returns the filename of a workspace recipe/append"""
26        metadata = name.split('/')[-1]
27        fn, _ = os.path.splitext(metadata)
28        return fn
29
30    if not os.path.exists(args.file):
31        raise DevtoolError('Tar archive %s does not exist. Export your workspace using "devtool export"' % args.file)
32
33    with tarfile.open(args.file) as tar:
34        # Get exported metadata
35        export_workspace_path = export_workspace = None
36        try:
37            metadata = tar.getmember(export.metadata)
38        except KeyError as ke:
39            raise DevtoolError('The export metadata file created by "devtool export" was not found. "devtool import" can only be used to import tar archives created by "devtool export".')
40
41        tar.extract(metadata)
42        with open(metadata.name) as fdm:
43            export_workspace_path, export_workspace = json.load(fdm)
44        os.unlink(metadata.name)
45
46        members = tar.getmembers()
47
48        # Get appends and recipes from the exported archive, these
49        # will be needed to find out those appends without corresponding
50        # recipe pair
51        append_fns, recipe_fns = set(), set()
52        for member in members:
53            if member.name.startswith('appends'):
54                append_fns.add(get_pn(member.name))
55            elif member.name.startswith('recipes'):
56                recipe_fns.add(get_pn(member.name))
57
58        # Setup tinfoil, get required data and shutdown
59        tinfoil = setup_tinfoil(config_only=False, basepath=basepath)
60        try:
61            current_fns = [os.path.basename(recipe[0]) for recipe in tinfoil.cooker.recipecaches[''].pkg_fn.items()]
62        finally:
63            tinfoil.shutdown()
64
65        # Find those appends that do not have recipes in current metadata
66        non_importables = []
67        for fn in append_fns - recipe_fns:
68            # Check on current metadata (covering those layers indicated in bblayers.conf)
69            for current_fn in current_fns:
70                if fnmatch.fnmatch(current_fn, '*' + fn.replace('%', '') + '*'):
71                    break
72            else:
73                non_importables.append(fn)
74                logger.warning('No recipe to append %s.bbapppend, skipping' % fn)
75
76        # Extract
77        imported = []
78        for member in members:
79            if member.name == export.metadata:
80                continue
81
82            for nonimp in non_importables:
83                pn = nonimp.split('_')[0]
84                # do not extract data from non-importable recipes or metadata
85                if member.name.startswith('appends/%s' % nonimp) or \
86                        member.name.startswith('recipes/%s' % nonimp) or \
87                        member.name.startswith('sources/%s' % pn):
88                    break
89            else:
90                path = os.path.join(config.workspace_path, member.name)
91                if os.path.exists(path):
92                    # by default, no file overwrite is done unless -o is given by the user
93                    if args.overwrite:
94                        try:
95                            tar.extract(member, path=config.workspace_path)
96                        except PermissionError as pe:
97                            logger.warning(pe)
98                    else:
99                        logger.warning('File already present. Use --overwrite/-o to overwrite it: %s' % member.name)
100                        continue
101                else:
102                    tar.extract(member, path=config.workspace_path)
103
104                # Update EXTERNALSRC and the devtool md5 file
105                if member.name.startswith('appends'):
106                    if export_workspace_path:
107                        # appends created by 'devtool modify' just need to update the workspace
108                        replace_from_file(path, export_workspace_path, config.workspace_path)
109
110                        # appends created by 'devtool add' need replacement of exported source tree
111                        pn = get_pn(member.name).split('_')[0]
112                        exported_srctree = export_workspace[pn]['srctree']
113                        if exported_srctree:
114                            replace_from_file(path, exported_srctree, os.path.join(config.workspace_path, 'sources', pn))
115
116                    standard._add_md5(config, pn, path)
117                    imported.append(pn)
118
119    if imported:
120        logger.info('Imported recipes into workspace %s: %s' % (config.workspace_path, ', '.join(imported)))
121    else:
122        logger.warning('No recipes imported into the workspace')
123
124    return 0
125
126def register_commands(subparsers, context):
127    """Register devtool import subcommands"""
128    parser = subparsers.add_parser('import',
129                                   help='Import exported tar archive into workspace',
130                                   description='Import tar archive previously created by "devtool export" into workspace',
131                                   group='advanced')
132    parser.add_argument('file', metavar='FILE', help='Name of the tar archive to import')
133    parser.add_argument('--overwrite', '-o', action="store_true", help='Overwrite files when extracting')
134    parser.set_defaults(func=devimport)
135