xref: /OK3568_Linux_fs/yocto/poky/scripts/lib/recipetool/append.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
1*4882a593Smuzhiyun# Recipe creation tool - append plugin
2*4882a593Smuzhiyun#
3*4882a593Smuzhiyun# Copyright (C) 2015 Intel Corporation
4*4882a593Smuzhiyun#
5*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only
6*4882a593Smuzhiyun#
7*4882a593Smuzhiyun
8*4882a593Smuzhiyunimport sys
9*4882a593Smuzhiyunimport os
10*4882a593Smuzhiyunimport argparse
11*4882a593Smuzhiyunimport glob
12*4882a593Smuzhiyunimport fnmatch
13*4882a593Smuzhiyunimport re
14*4882a593Smuzhiyunimport subprocess
15*4882a593Smuzhiyunimport logging
16*4882a593Smuzhiyunimport stat
17*4882a593Smuzhiyunimport shutil
18*4882a593Smuzhiyunimport scriptutils
19*4882a593Smuzhiyunimport errno
20*4882a593Smuzhiyunfrom collections import defaultdict
21*4882a593Smuzhiyun
22*4882a593Smuzhiyunlogger = logging.getLogger('recipetool')
23*4882a593Smuzhiyun
24*4882a593Smuzhiyuntinfoil = None
25*4882a593Smuzhiyun
26*4882a593Smuzhiyundef tinfoil_init(instance):
27*4882a593Smuzhiyun    global tinfoil
28*4882a593Smuzhiyun    tinfoil = instance
29*4882a593Smuzhiyun
30*4882a593Smuzhiyun
31*4882a593Smuzhiyun# FIXME guessing when we don't have pkgdata?
32*4882a593Smuzhiyun# FIXME mode to create patch rather than directly substitute
33*4882a593Smuzhiyun
34*4882a593Smuzhiyunclass InvalidTargetFileError(Exception):
35*4882a593Smuzhiyun    pass
36*4882a593Smuzhiyun
37*4882a593Smuzhiyundef find_target_file(targetpath, d, pkglist=None):
38*4882a593Smuzhiyun    """Find the recipe installing the specified target path, optionally limited to a select list of packages"""
39*4882a593Smuzhiyun    import json
40*4882a593Smuzhiyun
41*4882a593Smuzhiyun    pkgdata_dir = d.getVar('PKGDATA_DIR')
42*4882a593Smuzhiyun
43*4882a593Smuzhiyun    # The mix between /etc and ${sysconfdir} here may look odd, but it is just
44*4882a593Smuzhiyun    # being consistent with usage elsewhere
45*4882a593Smuzhiyun    invalidtargets = {'${sysconfdir}/version': '${sysconfdir}/version is written out at image creation time',
46*4882a593Smuzhiyun                      '/etc/timestamp': '/etc/timestamp is written out at image creation time',
47*4882a593Smuzhiyun                      '/dev/*': '/dev is handled by udev (or equivalent) and the kernel (devtmpfs)',
48*4882a593Smuzhiyun                      '/etc/passwd': '/etc/passwd should be managed through the useradd and extrausers classes',
49*4882a593Smuzhiyun                      '/etc/group': '/etc/group should be managed through the useradd and extrausers classes',
50*4882a593Smuzhiyun                      '/etc/shadow': '/etc/shadow should be managed through the useradd and extrausers classes',
51*4882a593Smuzhiyun                      '/etc/gshadow': '/etc/gshadow should be managed through the useradd and extrausers classes',
52*4882a593Smuzhiyun                      '${sysconfdir}/hostname': '${sysconfdir}/hostname contents should be set by setting hostname:pn-base-files = "value" in configuration',}
53*4882a593Smuzhiyun
54*4882a593Smuzhiyun    for pthspec, message in invalidtargets.items():
55*4882a593Smuzhiyun        if fnmatch.fnmatchcase(targetpath, d.expand(pthspec)):
56*4882a593Smuzhiyun            raise InvalidTargetFileError(d.expand(message))
57*4882a593Smuzhiyun
58*4882a593Smuzhiyun    targetpath_re = re.compile(r'\s+(\$D)?%s(\s|$)' % targetpath)
59*4882a593Smuzhiyun
60*4882a593Smuzhiyun    recipes = defaultdict(list)
61*4882a593Smuzhiyun    for root, dirs, files in os.walk(os.path.join(pkgdata_dir, 'runtime')):
62*4882a593Smuzhiyun        if pkglist:
63*4882a593Smuzhiyun            filelist = pkglist
64*4882a593Smuzhiyun        else:
65*4882a593Smuzhiyun            filelist = files
66*4882a593Smuzhiyun        for fn in filelist:
67*4882a593Smuzhiyun            pkgdatafile = os.path.join(root, fn)
68*4882a593Smuzhiyun            if pkglist and not os.path.exists(pkgdatafile):
69*4882a593Smuzhiyun                continue
70*4882a593Smuzhiyun            with open(pkgdatafile, 'r') as f:
71*4882a593Smuzhiyun                pn = ''
72*4882a593Smuzhiyun                # This does assume that PN comes before other values, but that's a fairly safe assumption
73*4882a593Smuzhiyun                for line in f:
74*4882a593Smuzhiyun                    if line.startswith('PN:'):
75*4882a593Smuzhiyun                        pn = line.split(': ', 1)[1].strip()
76*4882a593Smuzhiyun                    elif line.startswith('FILES_INFO'):
77*4882a593Smuzhiyun                        val = line.split(': ', 1)[1].strip()
78*4882a593Smuzhiyun                        dictval = json.loads(val)
79*4882a593Smuzhiyun                        for fullpth in dictval.keys():
80*4882a593Smuzhiyun                            if fnmatch.fnmatchcase(fullpth, targetpath):
81*4882a593Smuzhiyun                                recipes[targetpath].append(pn)
82*4882a593Smuzhiyun                    elif line.startswith('pkg_preinst:') or line.startswith('pkg_postinst:'):
83*4882a593Smuzhiyun                        scriptval = line.split(': ', 1)[1].strip().encode('utf-8').decode('unicode_escape')
84*4882a593Smuzhiyun                        if 'update-alternatives --install %s ' % targetpath in scriptval:
85*4882a593Smuzhiyun                            recipes[targetpath].append('?%s' % pn)
86*4882a593Smuzhiyun                        elif targetpath_re.search(scriptval):
87*4882a593Smuzhiyun                            recipes[targetpath].append('!%s' % pn)
88*4882a593Smuzhiyun    return recipes
89*4882a593Smuzhiyun
90*4882a593Smuzhiyundef _parse_recipe(pn, tinfoil):
91*4882a593Smuzhiyun    try:
92*4882a593Smuzhiyun        rd = tinfoil.parse_recipe(pn)
93*4882a593Smuzhiyun    except bb.providers.NoProvider as e:
94*4882a593Smuzhiyun        logger.error(str(e))
95*4882a593Smuzhiyun        return None
96*4882a593Smuzhiyun    return rd
97*4882a593Smuzhiyun
98*4882a593Smuzhiyundef determine_file_source(targetpath, rd):
99*4882a593Smuzhiyun    """Assuming we know a file came from a specific recipe, figure out exactly where it came from"""
100*4882a593Smuzhiyun    import oe.recipeutils
101*4882a593Smuzhiyun
102*4882a593Smuzhiyun    # See if it's in do_install for the recipe
103*4882a593Smuzhiyun    workdir = rd.getVar('WORKDIR')
104*4882a593Smuzhiyun    src_uri = rd.getVar('SRC_URI')
105*4882a593Smuzhiyun    srcfile = ''
106*4882a593Smuzhiyun    modpatches = []
107*4882a593Smuzhiyun    elements = check_do_install(rd, targetpath)
108*4882a593Smuzhiyun    if elements:
109*4882a593Smuzhiyun        logger.debug('do_install line:\n%s' % ' '.join(elements))
110*4882a593Smuzhiyun        srcpath = get_source_path(elements)
111*4882a593Smuzhiyun        logger.debug('source path: %s' % srcpath)
112*4882a593Smuzhiyun        if not srcpath.startswith('/'):
113*4882a593Smuzhiyun            # Handle non-absolute path
114*4882a593Smuzhiyun            srcpath = os.path.abspath(os.path.join(rd.getVarFlag('do_install', 'dirs').split()[-1], srcpath))
115*4882a593Smuzhiyun        if srcpath.startswith(workdir):
116*4882a593Smuzhiyun            # OK, now we have the source file name, look for it in SRC_URI
117*4882a593Smuzhiyun            workdirfile = os.path.relpath(srcpath, workdir)
118*4882a593Smuzhiyun            # FIXME this is where we ought to have some code in the fetcher, because this is naive
119*4882a593Smuzhiyun            for item in src_uri.split():
120*4882a593Smuzhiyun                localpath = bb.fetch2.localpath(item, rd)
121*4882a593Smuzhiyun                # Source path specified in do_install might be a glob
122*4882a593Smuzhiyun                if fnmatch.fnmatch(os.path.basename(localpath), workdirfile):
123*4882a593Smuzhiyun                    srcfile = 'file://%s' % localpath
124*4882a593Smuzhiyun                elif '/' in workdirfile:
125*4882a593Smuzhiyun                    if item == 'file://%s' % workdirfile:
126*4882a593Smuzhiyun                        srcfile = 'file://%s' % localpath
127*4882a593Smuzhiyun
128*4882a593Smuzhiyun        # Check patches
129*4882a593Smuzhiyun        srcpatches = []
130*4882a593Smuzhiyun        patchedfiles = oe.recipeutils.get_recipe_patched_files(rd)
131*4882a593Smuzhiyun        for patch, filelist in patchedfiles.items():
132*4882a593Smuzhiyun            for fileitem in filelist:
133*4882a593Smuzhiyun                if fileitem[0] == srcpath:
134*4882a593Smuzhiyun                    srcpatches.append((patch, fileitem[1]))
135*4882a593Smuzhiyun        if srcpatches:
136*4882a593Smuzhiyun            addpatch = None
137*4882a593Smuzhiyun            for patch in srcpatches:
138*4882a593Smuzhiyun                if patch[1] == 'A':
139*4882a593Smuzhiyun                    addpatch = patch[0]
140*4882a593Smuzhiyun                else:
141*4882a593Smuzhiyun                    modpatches.append(patch[0])
142*4882a593Smuzhiyun            if addpatch:
143*4882a593Smuzhiyun                srcfile = 'patch://%s' % addpatch
144*4882a593Smuzhiyun
145*4882a593Smuzhiyun    return (srcfile, elements, modpatches)
146*4882a593Smuzhiyun
147*4882a593Smuzhiyundef get_source_path(cmdelements):
148*4882a593Smuzhiyun    """Find the source path specified within a command"""
149*4882a593Smuzhiyun    command = cmdelements[0]
150*4882a593Smuzhiyun    if command in ['install', 'cp']:
151*4882a593Smuzhiyun        helptext = subprocess.check_output('LC_ALL=C %s --help' % command, shell=True).decode('utf-8')
152*4882a593Smuzhiyun        argopts = ''
153*4882a593Smuzhiyun        argopt_line_re = re.compile('^-([a-zA-Z0-9]), --[a-z-]+=')
154*4882a593Smuzhiyun        for line in helptext.splitlines():
155*4882a593Smuzhiyun            line = line.lstrip()
156*4882a593Smuzhiyun            res = argopt_line_re.search(line)
157*4882a593Smuzhiyun            if res:
158*4882a593Smuzhiyun                argopts += res.group(1)
159*4882a593Smuzhiyun        if not argopts:
160*4882a593Smuzhiyun            # Fallback
161*4882a593Smuzhiyun            if command == 'install':
162*4882a593Smuzhiyun                argopts = 'gmoSt'
163*4882a593Smuzhiyun            elif command == 'cp':
164*4882a593Smuzhiyun                argopts = 't'
165*4882a593Smuzhiyun            else:
166*4882a593Smuzhiyun                raise Exception('No fallback arguments for command %s' % command)
167*4882a593Smuzhiyun
168*4882a593Smuzhiyun        skipnext = False
169*4882a593Smuzhiyun        for elem in cmdelements[1:-1]:
170*4882a593Smuzhiyun            if elem.startswith('-'):
171*4882a593Smuzhiyun                if len(elem) > 1 and elem[1] in argopts:
172*4882a593Smuzhiyun                    skipnext = True
173*4882a593Smuzhiyun                continue
174*4882a593Smuzhiyun            if skipnext:
175*4882a593Smuzhiyun                skipnext = False
176*4882a593Smuzhiyun                continue
177*4882a593Smuzhiyun            return elem
178*4882a593Smuzhiyun    else:
179*4882a593Smuzhiyun        raise Exception('get_source_path: no handling for command "%s"')
180*4882a593Smuzhiyun
181*4882a593Smuzhiyundef get_func_deps(func, d):
182*4882a593Smuzhiyun    """Find the function dependencies of a shell function"""
183*4882a593Smuzhiyun    deps = bb.codeparser.ShellParser(func, logger).parse_shell(d.getVar(func))
184*4882a593Smuzhiyun    deps |= set((d.getVarFlag(func, "vardeps") or "").split())
185*4882a593Smuzhiyun    funcdeps = []
186*4882a593Smuzhiyun    for dep in deps:
187*4882a593Smuzhiyun        if d.getVarFlag(dep, 'func'):
188*4882a593Smuzhiyun            funcdeps.append(dep)
189*4882a593Smuzhiyun    return funcdeps
190*4882a593Smuzhiyun
191*4882a593Smuzhiyundef check_do_install(rd, targetpath):
192*4882a593Smuzhiyun    """Look at do_install for a command that installs/copies the specified target path"""
193*4882a593Smuzhiyun    instpath = os.path.abspath(os.path.join(rd.getVar('D'), targetpath.lstrip('/')))
194*4882a593Smuzhiyun    do_install = rd.getVar('do_install')
195*4882a593Smuzhiyun    # Handle where do_install calls other functions (somewhat crudely, but good enough for this purpose)
196*4882a593Smuzhiyun    deps = get_func_deps('do_install', rd)
197*4882a593Smuzhiyun    for dep in deps:
198*4882a593Smuzhiyun        do_install = do_install.replace(dep, rd.getVar(dep))
199*4882a593Smuzhiyun
200*4882a593Smuzhiyun    # Look backwards through do_install as we want to catch where a later line (perhaps
201*4882a593Smuzhiyun    # from a bbappend) is writing over the top
202*4882a593Smuzhiyun    for line in reversed(do_install.splitlines()):
203*4882a593Smuzhiyun        line = line.strip()
204*4882a593Smuzhiyun        if (line.startswith('install ') and ' -m' in line) or line.startswith('cp '):
205*4882a593Smuzhiyun            elements = line.split()
206*4882a593Smuzhiyun            destpath = os.path.abspath(elements[-1])
207*4882a593Smuzhiyun            if destpath == instpath:
208*4882a593Smuzhiyun                return elements
209*4882a593Smuzhiyun            elif destpath.rstrip('/') == os.path.dirname(instpath):
210*4882a593Smuzhiyun                # FIXME this doesn't take recursive copy into account; unsure if it's practical to do so
211*4882a593Smuzhiyun                srcpath = get_source_path(elements)
212*4882a593Smuzhiyun                if fnmatch.fnmatchcase(os.path.basename(instpath), os.path.basename(srcpath)):
213*4882a593Smuzhiyun                    return elements
214*4882a593Smuzhiyun    return None
215*4882a593Smuzhiyun
216*4882a593Smuzhiyun
217*4882a593Smuzhiyundef appendfile(args):
218*4882a593Smuzhiyun    import oe.recipeutils
219*4882a593Smuzhiyun
220*4882a593Smuzhiyun    stdout = ''
221*4882a593Smuzhiyun    try:
222*4882a593Smuzhiyun        (stdout, _) = bb.process.run('LANG=C file -b %s' % args.newfile, shell=True)
223*4882a593Smuzhiyun        if 'cannot open' in stdout:
224*4882a593Smuzhiyun            raise bb.process.ExecutionError(stdout)
225*4882a593Smuzhiyun    except bb.process.ExecutionError as err:
226*4882a593Smuzhiyun        logger.debug('file command returned error: %s' % err)
227*4882a593Smuzhiyun        stdout = ''
228*4882a593Smuzhiyun    if stdout:
229*4882a593Smuzhiyun        logger.debug('file command output: %s' % stdout.rstrip())
230*4882a593Smuzhiyun        if ('executable' in stdout and not 'shell script' in stdout) or 'shared object' in stdout:
231*4882a593Smuzhiyun            logger.warning('This file looks like it is a binary or otherwise the output of compilation. If it is, you should consider building it properly instead of substituting a binary file directly.')
232*4882a593Smuzhiyun
233*4882a593Smuzhiyun    if args.recipe:
234*4882a593Smuzhiyun        recipes = {args.targetpath: [args.recipe],}
235*4882a593Smuzhiyun    else:
236*4882a593Smuzhiyun        try:
237*4882a593Smuzhiyun            recipes = find_target_file(args.targetpath, tinfoil.config_data)
238*4882a593Smuzhiyun        except InvalidTargetFileError as e:
239*4882a593Smuzhiyun            logger.error('%s cannot be handled by this tool: %s' % (args.targetpath, e))
240*4882a593Smuzhiyun            return 1
241*4882a593Smuzhiyun        if not recipes:
242*4882a593Smuzhiyun            logger.error('Unable to find any package producing path %s - this may be because the recipe packaging it has not been built yet' % args.targetpath)
243*4882a593Smuzhiyun            return 1
244*4882a593Smuzhiyun
245*4882a593Smuzhiyun    alternative_pns = []
246*4882a593Smuzhiyun    postinst_pns = []
247*4882a593Smuzhiyun
248*4882a593Smuzhiyun    selectpn = None
249*4882a593Smuzhiyun    for targetpath, pnlist in recipes.items():
250*4882a593Smuzhiyun        for pn in pnlist:
251*4882a593Smuzhiyun            if pn.startswith('?'):
252*4882a593Smuzhiyun                alternative_pns.append(pn[1:])
253*4882a593Smuzhiyun            elif pn.startswith('!'):
254*4882a593Smuzhiyun                postinst_pns.append(pn[1:])
255*4882a593Smuzhiyun            elif selectpn:
256*4882a593Smuzhiyun                # hit here with multilibs
257*4882a593Smuzhiyun                continue
258*4882a593Smuzhiyun            else:
259*4882a593Smuzhiyun                selectpn = pn
260*4882a593Smuzhiyun
261*4882a593Smuzhiyun    if not selectpn and len(alternative_pns) == 1:
262*4882a593Smuzhiyun        selectpn = alternative_pns[0]
263*4882a593Smuzhiyun        logger.error('File %s is an alternative possibly provided by recipe %s but seemingly no other, selecting it by default - you should double check other recipes' % (args.targetpath, selectpn))
264*4882a593Smuzhiyun
265*4882a593Smuzhiyun    if selectpn:
266*4882a593Smuzhiyun        logger.debug('Selecting recipe %s for file %s' % (selectpn, args.targetpath))
267*4882a593Smuzhiyun        if postinst_pns:
268*4882a593Smuzhiyun            logger.warning('%s be modified by postinstall scripts for the following recipes:\n  %s\nThis may or may not be an issue depending on what modifications these postinstall scripts make.' % (args.targetpath, '\n  '.join(postinst_pns)))
269*4882a593Smuzhiyun        rd = _parse_recipe(selectpn, tinfoil)
270*4882a593Smuzhiyun        if not rd:
271*4882a593Smuzhiyun            # Error message already shown
272*4882a593Smuzhiyun            return 1
273*4882a593Smuzhiyun        sourcefile, instelements, modpatches = determine_file_source(args.targetpath, rd)
274*4882a593Smuzhiyun        sourcepath = None
275*4882a593Smuzhiyun        if sourcefile:
276*4882a593Smuzhiyun            sourcetype, sourcepath = sourcefile.split('://', 1)
277*4882a593Smuzhiyun            logger.debug('Original source file is %s (%s)' % (sourcepath, sourcetype))
278*4882a593Smuzhiyun            if sourcetype == 'patch':
279*4882a593Smuzhiyun                logger.warning('File %s is added by the patch %s - you may need to remove or replace this patch in order to replace the file.' % (args.targetpath, sourcepath))
280*4882a593Smuzhiyun                sourcepath = None
281*4882a593Smuzhiyun        else:
282*4882a593Smuzhiyun            logger.debug('Unable to determine source file, proceeding anyway')
283*4882a593Smuzhiyun        if modpatches:
284*4882a593Smuzhiyun            logger.warning('File %s is modified by the following patches:\n  %s' % (args.targetpath, '\n  '.join(modpatches)))
285*4882a593Smuzhiyun
286*4882a593Smuzhiyun        if instelements and sourcepath:
287*4882a593Smuzhiyun            install = None
288*4882a593Smuzhiyun        else:
289*4882a593Smuzhiyun            # Auto-determine permissions
290*4882a593Smuzhiyun            # Check destination
291*4882a593Smuzhiyun            binpaths = '${bindir}:${sbindir}:${base_bindir}:${base_sbindir}:${libexecdir}:${sysconfdir}/init.d'
292*4882a593Smuzhiyun            perms = '0644'
293*4882a593Smuzhiyun            if os.path.abspath(os.path.dirname(args.targetpath)) in rd.expand(binpaths).split(':'):
294*4882a593Smuzhiyun                # File is going into a directory normally reserved for executables, so it should be executable
295*4882a593Smuzhiyun                perms = '0755'
296*4882a593Smuzhiyun            else:
297*4882a593Smuzhiyun                # Check source
298*4882a593Smuzhiyun                st = os.stat(args.newfile)
299*4882a593Smuzhiyun                if st.st_mode & stat.S_IXUSR:
300*4882a593Smuzhiyun                    perms = '0755'
301*4882a593Smuzhiyun            install = {args.newfile: (args.targetpath, perms)}
302*4882a593Smuzhiyun        oe.recipeutils.bbappend_recipe(rd, args.destlayer, {args.newfile: sourcepath}, install, wildcardver=args.wildcard_version, machine=args.machine)
303*4882a593Smuzhiyun        return 0
304*4882a593Smuzhiyun    else:
305*4882a593Smuzhiyun        if alternative_pns:
306*4882a593Smuzhiyun            logger.error('File %s is an alternative possibly provided by the following recipes:\n  %s\nPlease select recipe with -r/--recipe' % (targetpath, '\n  '.join(alternative_pns)))
307*4882a593Smuzhiyun        elif postinst_pns:
308*4882a593Smuzhiyun            logger.error('File %s may be written out in a pre/postinstall script of the following recipes:\n  %s\nPlease select recipe with -r/--recipe' % (targetpath, '\n  '.join(postinst_pns)))
309*4882a593Smuzhiyun        return 3
310*4882a593Smuzhiyun
311*4882a593Smuzhiyun
312*4882a593Smuzhiyundef appendsrc(args, files, rd, extralines=None):
313*4882a593Smuzhiyun    import oe.recipeutils
314*4882a593Smuzhiyun
315*4882a593Smuzhiyun    srcdir = rd.getVar('S')
316*4882a593Smuzhiyun    workdir = rd.getVar('WORKDIR')
317*4882a593Smuzhiyun
318*4882a593Smuzhiyun    import bb.fetch
319*4882a593Smuzhiyun    simplified = {}
320*4882a593Smuzhiyun    src_uri = rd.getVar('SRC_URI').split()
321*4882a593Smuzhiyun    for uri in src_uri:
322*4882a593Smuzhiyun        if uri.endswith(';'):
323*4882a593Smuzhiyun            uri = uri[:-1]
324*4882a593Smuzhiyun        simple_uri = bb.fetch.URI(uri)
325*4882a593Smuzhiyun        simple_uri.params = {}
326*4882a593Smuzhiyun        simplified[str(simple_uri)] = uri
327*4882a593Smuzhiyun
328*4882a593Smuzhiyun    copyfiles = {}
329*4882a593Smuzhiyun    extralines = extralines or []
330*4882a593Smuzhiyun    for newfile, srcfile in files.items():
331*4882a593Smuzhiyun        src_destdir = os.path.dirname(srcfile)
332*4882a593Smuzhiyun        if not args.use_workdir:
333*4882a593Smuzhiyun            if rd.getVar('S') == rd.getVar('STAGING_KERNEL_DIR'):
334*4882a593Smuzhiyun                srcdir = os.path.join(workdir, 'git')
335*4882a593Smuzhiyun                if not bb.data.inherits_class('kernel-yocto', rd):
336*4882a593Smuzhiyun                    logger.warning('S == STAGING_KERNEL_DIR and non-kernel-yocto, unable to determine path to srcdir, defaulting to ${WORKDIR}/git')
337*4882a593Smuzhiyun            src_destdir = os.path.join(os.path.relpath(srcdir, workdir), src_destdir)
338*4882a593Smuzhiyun        src_destdir = os.path.normpath(src_destdir)
339*4882a593Smuzhiyun
340*4882a593Smuzhiyun        source_uri = 'file://{0}'.format(os.path.basename(srcfile))
341*4882a593Smuzhiyun        if src_destdir and src_destdir != '.':
342*4882a593Smuzhiyun            source_uri += ';subdir={0}'.format(src_destdir)
343*4882a593Smuzhiyun
344*4882a593Smuzhiyun        simple = bb.fetch.URI(source_uri)
345*4882a593Smuzhiyun        simple.params = {}
346*4882a593Smuzhiyun        simple_str = str(simple)
347*4882a593Smuzhiyun        if simple_str in simplified:
348*4882a593Smuzhiyun            existing = simplified[simple_str]
349*4882a593Smuzhiyun            if source_uri != existing:
350*4882a593Smuzhiyun                logger.warning('{0!r} is already in SRC_URI, with different parameters: {1!r}, not adding'.format(source_uri, existing))
351*4882a593Smuzhiyun            else:
352*4882a593Smuzhiyun                logger.warning('{0!r} is already in SRC_URI, not adding'.format(source_uri))
353*4882a593Smuzhiyun        else:
354*4882a593Smuzhiyun            extralines.append('SRC_URI += {0}'.format(source_uri))
355*4882a593Smuzhiyun        copyfiles[newfile] = srcfile
356*4882a593Smuzhiyun
357*4882a593Smuzhiyun    oe.recipeutils.bbappend_recipe(rd, args.destlayer, copyfiles, None, wildcardver=args.wildcard_version, machine=args.machine, extralines=extralines)
358*4882a593Smuzhiyun
359*4882a593Smuzhiyun
360*4882a593Smuzhiyundef appendsrcfiles(parser, args):
361*4882a593Smuzhiyun    recipedata = _parse_recipe(args.recipe, tinfoil)
362*4882a593Smuzhiyun    if not recipedata:
363*4882a593Smuzhiyun        parser.error('RECIPE must be a valid recipe name')
364*4882a593Smuzhiyun
365*4882a593Smuzhiyun    files = dict((f, os.path.join(args.destdir, os.path.basename(f)))
366*4882a593Smuzhiyun                 for f in args.files)
367*4882a593Smuzhiyun    return appendsrc(args, files, recipedata)
368*4882a593Smuzhiyun
369*4882a593Smuzhiyun
370*4882a593Smuzhiyundef appendsrcfile(parser, args):
371*4882a593Smuzhiyun    recipedata = _parse_recipe(args.recipe, tinfoil)
372*4882a593Smuzhiyun    if not recipedata:
373*4882a593Smuzhiyun        parser.error('RECIPE must be a valid recipe name')
374*4882a593Smuzhiyun
375*4882a593Smuzhiyun    if not args.destfile:
376*4882a593Smuzhiyun        args.destfile = os.path.basename(args.file)
377*4882a593Smuzhiyun    elif args.destfile.endswith('/'):
378*4882a593Smuzhiyun        args.destfile = os.path.join(args.destfile, os.path.basename(args.file))
379*4882a593Smuzhiyun
380*4882a593Smuzhiyun    return appendsrc(args, {args.file: args.destfile}, recipedata)
381*4882a593Smuzhiyun
382*4882a593Smuzhiyun
383*4882a593Smuzhiyundef layer(layerpath):
384*4882a593Smuzhiyun    if not os.path.exists(os.path.join(layerpath, 'conf', 'layer.conf')):
385*4882a593Smuzhiyun        raise argparse.ArgumentTypeError('{0!r} must be a path to a valid layer'.format(layerpath))
386*4882a593Smuzhiyun    return layerpath
387*4882a593Smuzhiyun
388*4882a593Smuzhiyun
389*4882a593Smuzhiyundef existing_path(filepath):
390*4882a593Smuzhiyun    if not os.path.exists(filepath):
391*4882a593Smuzhiyun        raise argparse.ArgumentTypeError('{0!r} must be an existing path'.format(filepath))
392*4882a593Smuzhiyun    return filepath
393*4882a593Smuzhiyun
394*4882a593Smuzhiyun
395*4882a593Smuzhiyundef existing_file(filepath):
396*4882a593Smuzhiyun    filepath = existing_path(filepath)
397*4882a593Smuzhiyun    if os.path.isdir(filepath):
398*4882a593Smuzhiyun        raise argparse.ArgumentTypeError('{0!r} must be a file, not a directory'.format(filepath))
399*4882a593Smuzhiyun    return filepath
400*4882a593Smuzhiyun
401*4882a593Smuzhiyun
402*4882a593Smuzhiyundef destination_path(destpath):
403*4882a593Smuzhiyun    if os.path.isabs(destpath):
404*4882a593Smuzhiyun        raise argparse.ArgumentTypeError('{0!r} must be a relative path, not absolute'.format(destpath))
405*4882a593Smuzhiyun    return destpath
406*4882a593Smuzhiyun
407*4882a593Smuzhiyun
408*4882a593Smuzhiyundef target_path(targetpath):
409*4882a593Smuzhiyun    if not os.path.isabs(targetpath):
410*4882a593Smuzhiyun        raise argparse.ArgumentTypeError('{0!r} must be an absolute path, not relative'.format(targetpath))
411*4882a593Smuzhiyun    return targetpath
412*4882a593Smuzhiyun
413*4882a593Smuzhiyun
414*4882a593Smuzhiyundef register_commands(subparsers):
415*4882a593Smuzhiyun    common = argparse.ArgumentParser(add_help=False)
416*4882a593Smuzhiyun    common.add_argument('-m', '--machine', help='Make bbappend changes specific to a machine only', metavar='MACHINE')
417*4882a593Smuzhiyun    common.add_argument('-w', '--wildcard-version', help='Use wildcard to make the bbappend apply to any recipe version', action='store_true')
418*4882a593Smuzhiyun    common.add_argument('destlayer', metavar='DESTLAYER', help='Base directory of the destination layer to write the bbappend to', type=layer)
419*4882a593Smuzhiyun
420*4882a593Smuzhiyun    parser_appendfile = subparsers.add_parser('appendfile',
421*4882a593Smuzhiyun                                              parents=[common],
422*4882a593Smuzhiyun                                              help='Create/update a bbappend to replace a target file',
423*4882a593Smuzhiyun                                              description='Creates a bbappend (or updates an existing one) to replace the specified file that appears in the target system, determining the recipe that packages the file and the required path and name for the bbappend automatically. Note that the ability to determine the recipe packaging a particular file depends upon the recipe\'s do_packagedata task having already run prior to running this command (which it will have when the recipe has been built successfully, which in turn will have happened if one or more of the recipe\'s packages is included in an image that has been built successfully).')
424*4882a593Smuzhiyun    parser_appendfile.add_argument('targetpath', help='Path to the file to be replaced (as it would appear within the target image, e.g. /etc/motd)', type=target_path)
425*4882a593Smuzhiyun    parser_appendfile.add_argument('newfile', help='Custom file to replace the target file with', type=existing_file)
426*4882a593Smuzhiyun    parser_appendfile.add_argument('-r', '--recipe', help='Override recipe to apply to (default is to find which recipe already packages the file)')
427*4882a593Smuzhiyun    parser_appendfile.set_defaults(func=appendfile, parserecipes=True)
428*4882a593Smuzhiyun
429*4882a593Smuzhiyun    common_src = argparse.ArgumentParser(add_help=False, parents=[common])
430*4882a593Smuzhiyun    common_src.add_argument('-W', '--workdir', help='Unpack file into WORKDIR rather than S', dest='use_workdir', action='store_true')
431*4882a593Smuzhiyun    common_src.add_argument('recipe', metavar='RECIPE', help='Override recipe to apply to')
432*4882a593Smuzhiyun
433*4882a593Smuzhiyun    parser = subparsers.add_parser('appendsrcfiles',
434*4882a593Smuzhiyun                                   parents=[common_src],
435*4882a593Smuzhiyun                                   help='Create/update a bbappend to add or replace source files',
436*4882a593Smuzhiyun                                   description='Creates a bbappend (or updates an existing one) to add or replace the specified file in the recipe sources, either those in WORKDIR or those in the source tree. This command lets you specify multiple files with a destination directory, so cannot specify the destination filename. See the `appendsrcfile` command for the other behavior.')
437*4882a593Smuzhiyun    parser.add_argument('-D', '--destdir', help='Destination directory (relative to S or WORKDIR, defaults to ".")', default='', type=destination_path)
438*4882a593Smuzhiyun    parser.add_argument('files', nargs='+', metavar='FILE', help='File(s) to be added to the recipe sources (WORKDIR or S)', type=existing_path)
439*4882a593Smuzhiyun    parser.set_defaults(func=lambda a: appendsrcfiles(parser, a), parserecipes=True)
440*4882a593Smuzhiyun
441*4882a593Smuzhiyun    parser = subparsers.add_parser('appendsrcfile',
442*4882a593Smuzhiyun                                   parents=[common_src],
443*4882a593Smuzhiyun                                   help='Create/update a bbappend to add or replace a source file',
444*4882a593Smuzhiyun                                   description='Creates a bbappend (or updates an existing one) to add or replace the specified files in the recipe sources, either those in WORKDIR or those in the source tree. This command lets you specify the destination filename, not just destination directory, but only works for one file. See the `appendsrcfiles` command for the other behavior.')
445*4882a593Smuzhiyun    parser.add_argument('file', metavar='FILE', help='File to be added to the recipe sources (WORKDIR or S)', type=existing_path)
446*4882a593Smuzhiyun    parser.add_argument('destfile', metavar='DESTFILE', nargs='?', help='Destination path (relative to S or WORKDIR, optional)', type=destination_path)
447*4882a593Smuzhiyun    parser.set_defaults(func=lambda a: appendsrcfile(parser, a), parserecipes=True)
448