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