1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3# Copyright 2014 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6"""Copies files to a directory.""" 7 8import filecmp 9import itertools 10import optparse 11import os 12import shutil 13import sys 14 15from util import build_utils 16 17 18def copy_tree(src, 19 dest, 20 follow_all_symlinks=False, 21 follow_outside_symlinks=False): 22 """copy src/* to dest/ 23 I. If follow_outside_symlinks is true, 24 1. If src item is a symlink, and points to some item inside src, 25 then copy the symlink to dest. 26 2. If src item points to items outside src, then follow links to copy 27 the original file to dest. 28 3. Else copy src item to dest. 29 II. If follow_all_symlinks is true, 30 1. If src item is a symlink, then follow links to copy the original file 31 to dest. 32 2. Else copy src item to dest. 33 follow_outside_symlinks is true when follow_all_symlinks is true. 34 """ 35 with os.scandir(src) as itr: 36 items = list(itr) 37 return _do_copy_tree(items, 38 src, 39 dest, 40 follow_all_symlinks=follow_all_symlinks, 41 follow_outside_symlinks=follow_outside_symlinks) 42 43 44def _do_copy_tree(items, 45 src, 46 dest, 47 follow_all_symlinks=False, 48 follow_outside_symlinks=False): 49 os.makedirs(dest, exist_ok=True) 50 for item in items: 51 srcname = os.path.join(src, item.name) 52 destname = os.path.join(dest, item.name) 53 is_symlink = item.is_symlink() 54 if is_symlink: 55 org_linkto = os.readlink(srcname) 56 linkto = org_linkto 57 if not os.path.isabs(org_linkto): 58 linkto = os.path.join(os.path.dirname(item), org_linkto) 59 60 if not os.path.exists(linkto): 61 os.symlink(org_linkto, destname) 62 shutil.copymode(srcname, destname, follow_symlinks=False) 63 continue 64 65 if follow_all_symlinks: 66 if item.is_dir(): 67 copy_tree(item, 68 destname, 69 follow_all_symlinks=follow_all_symlinks, 70 follow_outside_symlinks=follow_outside_symlinks) 71 else: 72 shutil.copy(item, destname) 73 shutil.copymode(item, destname) 74 75 elif follow_outside_symlinks: 76 if os.path.abspath(src) in os.path.abspath( 77 linkto) and not os.path.isabs(org_linkto): 78 os.symlink(org_linkto, destname) 79 shutil.copymode(srcname, destname, follow_symlinks=False) 80 else: 81 if item.is_dir(): 82 copy_tree( 83 item, 84 destname, 85 follow_all_symlinks=follow_all_symlinks, 86 follow_outside_symlinks=follow_outside_symlinks) 87 else: 88 shutil.copy(item, destname) 89 shutil.copymode(item, destname) 90 else: 91 os.symlink(org_linkto, destname) 92 shutil.copymode(srcname, destname, follow_symlinks=False) 93 elif item.is_dir(): 94 copy_tree(item, 95 destname, 96 follow_all_symlinks=follow_all_symlinks, 97 follow_outside_symlinks=follow_outside_symlinks) 98 else: 99 shutil.copy(item, destname) 100 shutil.copymode(item, destname) 101 shutil.copystat(src, dest) 102 return dest 103 104 105def copy_file(f, 106 dest, 107 deps, 108 follow_all_symlinks=False, 109 follow_outside_symlinks=False): 110 """Copy file or directory and update deps.""" 111 if os.path.isdir(f): 112 copy_tree(f, 113 os.path.join(dest, os.path.basename(f)), 114 follow_all_symlinks=follow_all_symlinks, 115 follow_outside_symlinks=follow_outside_symlinks) 116 deps.extend(build_utils.get_all_files(f)) 117 else: 118 if os.path.isfile(os.path.join(dest, os.path.basename(f))): 119 dest = os.path.join(dest, os.path.basename(f)) 120 121 deps.append(f) 122 123 if os.path.isfile(dest): 124 if filecmp.cmp(dest, f, shallow=False): 125 return 126 # The shutil.copy() below would fail if the file does not 127 # have write permissions. Deleting the file 128 # has similar costs to modifying the permissions. 129 os.unlink(dest) 130 131 shutil.copy(f, dest) 132 133 134def do_copy(options, deps): 135 """Copy files or directories given in options.files and update deps.""" 136 files = list( 137 itertools.chain.from_iterable( 138 build_utils.parse_gn_list(f) for f in options.files)) 139 140 for f in files: 141 if not options.ignore_stale: 142 if os.path.isdir(f) and not options.clear: 143 print('To avoid stale files you must use --clear when copying ' 144 'directories') 145 sys.exit(-1) 146 copy_file(f, 147 options.dest, 148 deps, 149 follow_all_symlinks=options.follow_all_symlinks, 150 follow_outside_symlinks=options.follow_outside_symlinks) 151 152 153def do_renaming(options, deps): 154 """Copy and rename files given in options.renaming_sources 155 and update deps. 156 """ 157 src_files = list( 158 itertools.chain.from_iterable( 159 build_utils.parse_gn_list(f) for f in options.renaming_sources)) 160 161 dest_files = list( 162 itertools.chain.from_iterable( 163 build_utils.parse_gn_list(f) 164 for f in options.renaming_destinations)) 165 166 if (len(src_files) != len(dest_files)): 167 print('Renaming source and destination files not match.') 168 sys.exit(-1) 169 170 for src, dest in zip(src_files, dest_files): 171 if os.path.isdir(src): 172 print('renaming directory is not supported.') 173 sys.exit(-1) 174 else: 175 copy_file(src, os.path.join(options.dest, dest), deps) 176 177 178def main(args): 179 args = build_utils.expand_file_args(args) 180 181 parser = optparse.OptionParser() 182 build_utils.add_depfile_option(parser) 183 184 parser.add_option('--dest', help='Directory to copy files to.') 185 parser.add_option('--files', 186 action='append', 187 help='List of files to copy.') 188 parser.add_option( 189 '--clear', 190 action='store_true', 191 help='If set, the destination directory will be deleted ' 192 'before copying files to it. This is highly recommended to ' 193 'ensure that no stale files are left in the directory.') 194 parser.add_option('--ignore-stale', 195 action='store_true', 196 help='Do copy even there may be stale files. ' 197 'If set, overrule options.clear') 198 parser.add_option('--stamp', help='Path to touch on success.') 199 parser.add_option('--follow-all-symlinks', 200 action='store_true', 201 help='whether to follow symlinks') 202 parser.add_option( 203 '--follow-outside-symlinks', 204 action='store_true', 205 help='whether to follow symlinks which points to targets outside of' 206 'source directory') 207 parser.add_option('--renaming-sources', 208 action='append', 209 help='List of files need to be renamed while being ' 210 'copied to dest directory') 211 parser.add_option('--renaming-destinations', 212 action='append', 213 help='List of destination file name without path, the ' 214 'number of elements must match rename-sources.') 215 216 options, _ = parser.parse_args(args) 217 if options.follow_all_symlinks: 218 options.follow_outside_symlinks = True 219 220 if options.clear and not options.ignore_stale: 221 build_utils.delete_directory(options.dest) 222 build_utils.make_directory(options.dest) 223 224 deps = [] 225 226 if options.files: 227 do_copy(options, deps) 228 229 if options.renaming_sources: 230 do_renaming(options, deps) 231 232 if options.depfile: 233 build_utils.write_depfile(options.depfile, 234 options.stamp, 235 deps, 236 add_pydeps=False) 237 238 if options.stamp: 239 build_utils.touch(options.stamp) 240 241 242if __name__ == '__main__': 243 sys.exit(main(sys.argv[1:])) 244