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