OLD | NEW |
| (Empty) |
1 # Copyright 2016 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 import argparse | |
6 import errno | |
7 import os | |
8 import shutil | |
9 import subprocess | |
10 import sys | |
11 | |
12 # Copies a macOS or iOS resource into a bundle. Special handling is given to | |
13 # .strings files. | |
14 # | |
15 # Usage: python copy_bundle_data.py path/to/source path/of/destination | |
16 # | |
17 # Note that if |source| is a directory, its contents will be copied to |dest|, | |
18 # rather than copying the |source| directory itself. Example: | |
19 # copy_bundle_data.py out/Release/Foo.framework out/Release/App/Foo.framework | |
20 # The contents of Foo.framwork will be copied into the destination path, which | |
21 # includes the name of the destination framework, which is also Foo.framework. | |
22 | |
23 def DetectEncoding(data, default_encoding='UTF-8'): | |
24 """Detects the encoding used by |data| from the Byte-Order-Mark if present. | |
25 | |
26 Args: | |
27 data: string whose encoding needs to be detected | |
28 default_encoding: encoding returned if no BOM is found. | |
29 | |
30 Returns: | |
31 The encoding determined from the BOM if present or |default_encoding| if | |
32 no BOM was found. | |
33 """ | |
34 if data.startswith('\xFE\xFF'): | |
35 return 'UTF-16BE' | |
36 | |
37 if data.startswith('\xFF\xFE'): | |
38 return 'UTF-16LE' | |
39 | |
40 if data.startswith('\xEF\xBB\xBF'): | |
41 return 'UTF-8' | |
42 | |
43 return default_encoding | |
44 | |
45 | |
46 def CopyStringsFile(source, dest, strings_format): | |
47 """Copies a .strings file from |source| to |dest| and convert it to UTF-16. | |
48 | |
49 Args: | |
50 source: string, path to the source file | |
51 dest: string, path to the destination file | |
52 """ | |
53 with open(source, 'rb') as source_file: | |
54 data = source_file.read() | |
55 | |
56 # Xcode's CpyCopyStringsFile / builtin-copyStrings seems to call | |
57 # CFPropertyListCreateFromXMLData() behind the scenes; at least it prints | |
58 # CFPropertyListCreateFromXMLData(): Old-style plist parser: missing | |
59 # semicolon in dictionary. | |
60 # on invalid files. Do the same kind of validation. | |
61 import CoreFoundation as CF | |
62 cfdata = CF.CFDataCreate(None, data, len(data)) | |
63 plist, error = CF.CFPropertyListCreateFromXMLData(None, cfdata, 0, None) | |
64 if error: | |
65 raise ValueError(error) | |
66 | |
67 if strings_format == 'legacy': | |
68 encoding = DetectEncoding(data) | |
69 with open(dest, 'wb') as dest_file: | |
70 dest_file.write(data.decode(encoding).encode('UTF-16')) | |
71 else: | |
72 cfformat = { | |
73 'xml1': CF.kCFPropertyListXMLFormat_v1_0, | |
74 'binary1': CF.kCFPropertyListBinaryFormat_v1_0, | |
75 }[strings_format] | |
76 cfdata, error = CF.CFPropertyListCreateData( | |
77 None, plist, CF.kCFPropertyListBinaryFormat_v1_0, | |
78 0, None) | |
79 if error: | |
80 raise ValueError(error) | |
81 | |
82 data = CF.CFDataGetBytes( | |
83 cfdata, CF.CFRangeMake(0, CF.CFDataGetLength(cfdata)), None) | |
84 with open(dest, 'wb') as dest_file: | |
85 dest_file.write(data) | |
86 | |
87 | |
88 def CopyFile(source, dest, strings_format): | |
89 """Copies a file or directory from |source| to |dest|. | |
90 | |
91 Args: | |
92 source: string, path to the source file | |
93 dest: string, path to the destination file | |
94 """ | |
95 try: | |
96 shutil.rmtree(dest) | |
97 except OSError as e: | |
98 if e.errno == errno.ENOENT: | |
99 pass | |
100 elif e.errno == errno.ENOTDIR: | |
101 os.unlink(dest) | |
102 else: | |
103 raise | |
104 | |
105 _, extension = os.path.splitext(source) | |
106 if extension == '.strings': | |
107 CopyStringsFile(source, dest, strings_format) | |
108 return | |
109 | |
110 # If the source is a directory, add a trailing slash so its contents get | |
111 # copied, rather than copying the directory itself. | |
112 if os.path.isdir(source) and not source.endswith('/'): | |
113 source += '/' | |
114 | |
115 subprocess.check_call( | |
116 ['rsync', '--recursive', '--perms', '--links', source, dest]) | |
117 | |
118 | |
119 def Main(): | |
120 parser = argparse.ArgumentParser( | |
121 description='copy source to destination for the creation of a bundle') | |
122 parser.add_argument('--strings-format', | |
123 choices=('xml1', 'binary1', 'legacy'), default='legacy', | |
124 help='convert .strings file to format (default: %(default)s)') | |
125 parser.add_argument('source', help='path to source file or directory') | |
126 parser.add_argument('dest', help='path to destination') | |
127 args = parser.parse_args() | |
128 | |
129 CopyFile(args.source, args.dest, args.strings_format) | |
130 | |
131 if __name__ == '__main__': | |
132 sys.exit(Main()) | |
OLD | NEW |