OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """A simple project generator for Native Client projects written in C or C++. | |
7 | |
8 This script accepts a few argument which it uses as a description of a new NaCl | |
9 project. It sets up a project with a given name and a given primary language | |
10 (default: C++, optionally, C) using the appropriate files from this area. | |
11 This script does not handle setup for complex applications, just the basic | |
12 necessities to get a functional native client application stub. When this | |
13 script terminates a compileable project stub will exist with the specified | |
14 name, at the specified location. | |
15 | |
16 GetCamelCaseName(): Converts an underscore name to a camel case name. | |
17 GetCodeDirectory(): Decides what directory to pull source code from. | |
18 GetCodeSoureFiles(): Decides what source files to pull into the stub. | |
19 GetCommonSourceFiles(): Gives list of files needed by all project types. | |
20 GetHTMLDirectory(): Decides what directory to pull HTML stub from. | |
21 GetHTMLSourceFiles(): Gives HTML files to be included in project stub. | |
22 GetTargetFileName(): Converts a source file name into a project file name. | |
23 ParseArguments(): Parses the arguments provided by the user. | |
24 ReplaceInFile(): Replaces a given string with another in a given file. | |
25 ProjectInitializer: Maintains some state applicable to setting up a project. | |
26 main(): Executes the script. | |
27 """ | |
28 | |
29 __author__ = 'mlinck@google.com (Michael Linck)' | |
30 | |
31 import fileinput | |
32 import optparse | |
33 import os.path | |
34 import shutil | |
35 import sys | |
36 import uuid | |
37 | |
38 # A list of all platforms that should have make.cmd. | |
39 WINDOWS_BUILD_PLATFORMS = ['cygwin', 'win32'] | |
40 | |
41 # Tags that will be replaced in our the new project's source files. | |
42 PROJECT_NAME_TAG = '<PROJECT_NAME>' | |
43 PROJECT_NAME_CAMEL_CASE_TAG = '<ProjectName>' | |
44 SDK_ROOT_TAG = '<NACL_SDK_ROOT>' | |
45 NACL_PLATFORM_TAG = '<NACL_PLATFORM>' | |
46 VS_PROJECT_UUID_TAG = '<VS_PROJECT_UUID>' | |
47 VS_SOURCE_UUID_TAG = '<VS_SOURCE_UUID>' | |
48 VS_HEADER_UUID_TAG = '<VS_HEADER_UUID>' | |
49 VS_RESOURCE_UUID_TAG = '<VS_RESOURCE_UUID>' | |
50 | |
51 # This string is the part of the file name that will be replaced. | |
52 PROJECT_FILE_NAME = 'project_file' | |
53 | |
54 # Lists of source files that will be used for the new project. | |
55 COMMON_PROJECT_FILES = ['scons'] | |
56 C_SOURCE_FILES = ['build.scons', '%s.c' % PROJECT_FILE_NAME] | |
57 CC_SOURCE_FILES = ['build.scons', '%s.cc' % PROJECT_FILE_NAME] | |
58 HTML_FILES = ['%s.html' % PROJECT_FILE_NAME] | |
59 VS_FILES = ['%s.sln' % PROJECT_FILE_NAME, '%s.vcproj' % PROJECT_FILE_NAME] | |
60 | |
61 # Error needs to be a class, since we 'raise' it in several places. | |
62 class Error(Exception): | |
63 pass | |
64 | |
65 | |
66 def GetCamelCaseName(lower_case_name): | |
67 """Converts an underscore name to a camel case name. | |
68 | |
69 Args: | |
70 lower_case_name: The name in underscore-delimited lower case format. | |
71 | |
72 Returns: | |
73 The name in camel case format. | |
74 """ | |
75 camel_case_name = '' | |
76 name_parts = lower_case_name.split('_') | |
77 for part in name_parts: | |
78 if part: | |
79 camel_case_name += part.capitalize() | |
80 return camel_case_name | |
81 | |
82 | |
83 def GetCodeDirectory(is_c_project, project_templates_dir): | |
84 """Decides what directory to pull source code from. | |
85 | |
86 Args: | |
87 is_c_project: A boolean indicating whether this project is in C or not. | |
88 project_templates_dir: The path to the project_templates directory. | |
89 | |
90 Returns: | |
91 The code directory for the given project type. | |
92 """ | |
93 stub_directory = '' | |
94 if is_c_project: | |
95 stub_directory = os.path.join(project_templates_dir, 'c') | |
96 else: | |
97 stub_directory = os.path.join(project_templates_dir, 'cc') | |
98 return stub_directory | |
99 | |
100 | |
101 def GetCodeSourceFiles(is_c_project): | |
102 """Decides what source files to pull into the stub. | |
103 | |
104 Args: | |
105 is_c_project: A boolean indicating whether this project is in C or not. | |
106 | |
107 Returns: | |
108 The files that are specific to the requested type of project and live in its | |
109 directory. | |
110 """ | |
111 project_files = [] | |
112 if is_c_project: | |
113 project_files = C_SOURCE_FILES | |
114 else: | |
115 project_files = CC_SOURCE_FILES | |
116 return project_files | |
117 | |
118 | |
119 def GetCommonSourceFiles(): | |
120 """Gives list of files needed by all project types. | |
121 | |
122 Returns: | |
123 The files C and C++ projects have in common. These are the files that live | |
124 in the top level project_templates directory. | |
125 """ | |
126 project_files = COMMON_PROJECT_FILES | |
127 if sys.platform in WINDOWS_BUILD_PLATFORMS: | |
128 project_files.extend(['scons.bat']) | |
129 return project_files | |
130 | |
131 | |
132 def GetVsDirectory(project_templates_dir): | |
133 """Decides what directory to pull Visual Studio stub from. | |
134 | |
135 Args: | |
136 project_templates_dir: The path to the project_templates directory. | |
137 | |
138 Returns: | |
139 The directory where the HTML stub is to be found. | |
140 """ | |
141 return os.path.join(project_templates_dir, 'vs') | |
142 | |
143 | |
144 def GetVsProjectFiles(): | |
145 """Gives VisualStudio files to be included in project stub. | |
146 | |
147 Returns: | |
148 The VisualStudio files needed for the project. | |
149 """ | |
150 return VS_FILES | |
151 | |
152 | |
153 def GetHTMLDirectory(project_templates_dir): | |
154 """Decides what directory to pull HTML stub from. | |
155 | |
156 Args: | |
157 project_templates_dir: The path to the project_templates directory. | |
158 | |
159 Returns: | |
160 The directory where the HTML stub is to be found. | |
161 """ | |
162 return os.path.join(project_templates_dir, 'html') | |
163 | |
164 | |
165 def GetHTMLSourceFiles(): | |
166 """Gives HTML files to be included in project stub. | |
167 | |
168 Returns: | |
169 The HTML files needed for the project. | |
170 """ | |
171 return HTML_FILES | |
172 | |
173 | |
174 def GetTargetFileName(source_file_name, project_name): | |
175 """Converts a source file name into a project file name. | |
176 | |
177 Args: | |
178 source_file_name: The name of a file that is to be included in the project | |
179 stub, as it appears at the source location. | |
180 project_name: The name of the project that is being generated. | |
181 | |
182 Returns: | |
183 The target file name for a given source file. All project files are run | |
184 through this filter and it modifies them as needed. | |
185 """ | |
186 target_file_name = '' | |
187 if source_file_name.startswith(PROJECT_FILE_NAME): | |
188 target_file_name = source_file_name.replace(PROJECT_FILE_NAME, | |
189 project_name) | |
190 else: | |
191 target_file_name = source_file_name | |
192 return target_file_name | |
193 | |
194 | |
195 def GetDefaultProjectDir(): | |
196 """Determines the default project directory. | |
197 | |
198 The default directory root for new projects is called 'nacl_projects' under | |
199 the user's home directory. There are two ways to override this: you can set | |
200 the NACL_PROJECT_ROOT environment variable, or use the --directory option. | |
201 | |
202 Returns: | |
203 An os-specific path to the default project directory, which is called | |
204 'nacl_projects' under the user's home directory. | |
205 """ | |
206 return os.getenv('NACL_PROJECT_ROOT', | |
207 os.path.join(os.path.expanduser('~'), 'nacl_projects')) | |
208 | |
209 | |
210 def ParseArguments(argv): | |
211 """Parses the arguments provided by the user. | |
212 | |
213 Parses the command line options and makes sure the script errors when it is | |
214 supposed to. | |
215 | |
216 Args: | |
217 argv: The argument array. | |
218 | |
219 Returns: | |
220 The options structure that represents the arguments after they have been | |
221 parsed. | |
222 """ | |
223 parser = optparse.OptionParser() | |
224 parser.add_option( | |
225 '-n', '--name', dest='project_name', | |
226 default='', | |
227 help=('Required: the name of the new project to be stubbed out.\n' | |
228 'Please use lower case names with underscore, i.e. hello_world.')) | |
229 parser.add_option( | |
230 '-d', '--directory', dest='project_directory', | |
231 default=GetDefaultProjectDir(), | |
232 help=('Optional: If set, the new project will be created under this ' | |
233 'directory and the directory created if necessary.')) | |
234 parser.add_option( | |
235 '-c', action='store_true', dest='is_c_project', | |
236 default=False, | |
237 help=('Optional: If set, this will generate a C project. Default ' | |
238 'is C++.')) | |
239 parser.add_option( | |
240 '-p', '--nacl-platform', dest='nacl_platform', | |
241 default='pepper_17', | |
242 help=('Optional: if set, the new project will target the given nacl\n' | |
243 'platform. Default is the most current platform. e.g. pepper_17')) | |
244 parser.add_option( | |
245 '--vsproj', action='store_true', dest='is_vs_project', | |
246 default=False, | |
247 help=('Optional: If set, generate Visual Studio project files.')) | |
248 result = parser.parse_args(argv) | |
249 options = result[0] | |
250 args = result[1] | |
251 #options, args) = parser.parse_args(argv) | |
252 if args: | |
253 parser.print_help() | |
254 sys.exit(1) | |
255 elif not options.project_name.islower(): | |
256 print('--name missing or in incorrect format. Please use -h for ' | |
257 'instructions.') | |
258 sys.exit(1) | |
259 return options | |
260 | |
261 | |
262 class ProjectInitializer(object): | |
263 """Maintains the state of the project that is being created.""" | |
264 | |
265 def __init__(self, is_c_project, is_vs_project, project_name, | |
266 project_location, nacl_platform, project_templates_dir, | |
267 nacl_sdk_root=None, os_resource=os): | |
268 """Initializes all the fields that are known after parsing the parameters. | |
269 | |
270 Args: | |
271 is_c_project: A boolean indicating whether this project is in C or not. | |
272 is_vs_project: A boolean indicating whether this project has Visual | |
273 Studio support. | |
274 project_name: A string containing the name of the project to be created. | |
275 project_location: A path indicating where the new project is to be placed. | |
276 project_templates_dir: The path to the project_templates directory. | |
277 os_resource: A resource to be used as os. Provided for unit testing. | |
278 """ | |
279 self.__is_c_project = is_c_project | |
280 self.__is_vs_project = is_vs_project | |
281 self.__project_files = [] | |
282 self.__project_dir = None | |
283 self.__project_name = project_name | |
284 self.__project_location = project_location | |
285 self.__nacl_platform = nacl_platform | |
286 self.__project_templates_dir = project_templates_dir | |
287 # System resources are properties so mocks can be inserted. | |
288 self.__fileinput = fileinput | |
289 self.__nacl_sdk_root = nacl_sdk_root | |
290 self.__os = os_resource | |
291 self.__shutil = shutil | |
292 self.__sys = sys | |
293 self.__CreateProjectDirectory() | |
294 | |
295 def CopyAndRenameFiles(self, source_dir, file_names): | |
296 """Places files in the new project's directory and renames them as needed. | |
297 | |
298 Copies the given files from the given source directory into the new | |
299 project's directory, renaming them as necessary. Each file that is created | |
300 in the project directory is also added to self.__project_files. | |
301 | |
302 Args: | |
303 source_dir: A path indicating where the files are to be copied from. | |
304 file_names: The list of files that is to be copied out of source_dir. | |
305 """ | |
306 for source_file_name in file_names: | |
307 target_file_name = GetTargetFileName(source_file_name, | |
308 self.__project_name) | |
309 copy_source_file = self.os.path.join(source_dir, source_file_name) | |
310 copy_target_file = self.os.path.join(self.__project_dir, target_file_name) | |
311 self.shutil.copy(copy_source_file, copy_target_file) | |
312 self.__project_files += [copy_target_file] | |
313 | |
314 def __CreateProjectDirectory(self): | |
315 """Creates the project's directory and any parents as necessary.""" | |
316 self.__project_dir = self.os.path.join(self.__project_location, | |
317 self.__project_name) | |
318 if self.os.path.exists(self.__project_dir): | |
319 raise Error("Error: directory '%s' already exists" % self.__project_dir) | |
320 self.os.makedirs(self.__project_dir) | |
321 | |
322 def PrepareDirectoryContent(self): | |
323 """Prepares the directory for the new project. | |
324 | |
325 This function's job is to know what directories need to be used and what | |
326 files need to be copied and renamed. It uses several tiny helper functions | |
327 to do this. | |
328 There are three locations from which files are copied to create a project. | |
329 That number may change in the future. | |
330 """ | |
331 code_source_dir = GetCodeDirectory(self.__is_c_project, | |
332 self.__project_templates_dir) | |
333 code_source_files = GetCodeSourceFiles(self.__is_c_project) | |
334 html_source_dir = GetHTMLDirectory(self.__project_templates_dir) | |
335 html_source_files = GetHTMLSourceFiles() | |
336 common_source_files = GetCommonSourceFiles() | |
337 self.CopyAndRenameFiles(code_source_dir, code_source_files) | |
338 self.CopyAndRenameFiles(html_source_dir, html_source_files) | |
339 self.CopyAndRenameFiles(self.__project_templates_dir, | |
340 common_source_files) | |
341 if self.__is_vs_project: | |
342 vs_source_dir = GetVsDirectory(self.__project_templates_dir) | |
343 vs_files = GetVsProjectFiles() | |
344 self.CopyAndRenameFiles(vs_source_dir, vs_files) | |
345 print('init_project has copied the appropriate files to: %s' % | |
346 self.__project_dir) | |
347 | |
348 def PrepareFileContent(self): | |
349 """Changes contents of files in the new project as needed. | |
350 | |
351 Goes through each file in the project that is being created and replaces | |
352 contents as necessary. | |
353 """ | |
354 camel_case_name = GetCamelCaseName(self.__project_name) | |
355 sdk_root_dir = self.__nacl_sdk_root | |
356 if not sdk_root_dir: | |
357 raise Error("Error: NACL_SDK_ROOT is not set") | |
358 sdk_root_dir = self.os.path.abspath(sdk_root_dir) | |
359 if self.__is_vs_project: | |
360 project_uuid = str(uuid.uuid4()).upper() | |
361 vs_source_uuid = str(uuid.uuid4()).upper() | |
362 vs_header_uuid = str(uuid.uuid4()).upper() | |
363 vs_resource_uuid = str(uuid.uuid4()).upper() | |
364 for project_file in self.__project_files: | |
365 self.ReplaceInFile(project_file, PROJECT_NAME_TAG, self.__project_name) | |
366 self.ReplaceInFile(project_file, | |
367 PROJECT_NAME_CAMEL_CASE_TAG, | |
368 camel_case_name) | |
369 self.ReplaceInFile(project_file, SDK_ROOT_TAG, sdk_root_dir) | |
370 self.ReplaceInFile(project_file, NACL_PLATFORM_TAG, self.__nacl_platform) | |
371 if self.__is_vs_project: | |
372 self.ReplaceInFile(project_file, VS_PROJECT_UUID_TAG, project_uuid) | |
373 self.ReplaceInFile(project_file, VS_SOURCE_UUID_TAG, vs_source_uuid) | |
374 self.ReplaceInFile(project_file, VS_HEADER_UUID_TAG, vs_header_uuid) | |
375 self.ReplaceInFile(project_file, VS_RESOURCE_UUID_TAG, vs_resource_uuid) | |
376 | |
377 def ReplaceInFile(self, file_path, old_text, new_text): | |
378 """Replaces a given string with another in a given file. | |
379 | |
380 Args: | |
381 file_path: The path to the file that is to be modified. | |
382 old_text: The text that is to be removed. | |
383 new_text: The text that is to be added in place of old_text. | |
384 """ | |
385 for line in self.fileinput.input(file_path, inplace=1, mode='U'): | |
386 self.sys.stdout.write(line.replace(old_text, new_text)) | |
387 | |
388 # The following properties exist to make unit testing possible. | |
389 | |
390 def _GetFileinput(self): | |
391 """Accessor for Fileinput property.""" | |
392 return self.__fileinput | |
393 | |
394 def __GetFileinput(self): | |
395 """Indirect Accessor for _GetFileinput.""" | |
396 return self._GetFileinput() | |
397 | |
398 def _SetFileinput(self, fileinput_resource): | |
399 """Accessor for Fileinput property.""" | |
400 self.__fileinput = fileinput_resource | |
401 | |
402 def __SetFileinput(self, fileinput_resource): | |
403 """Indirect Accessor for _SetFileinput.""" | |
404 return self._SetFileinput(fileinput_resource) | |
405 | |
406 fileinput = property( | |
407 __GetFileinput, __SetFileinput, | |
408 doc="""Gets and sets the resource to use as fileinput.""") | |
409 | |
410 def _GetOS(self): | |
411 """Accessor for os property.""" | |
412 return self.__os | |
413 | |
414 def __GetOS(self): | |
415 """Indirect Accessor for _GetOS.""" | |
416 return self._GetOS() | |
417 | |
418 def _SetOS(self, os_resource): | |
419 """Accessor for os property.""" | |
420 self.__os = os_resource | |
421 | |
422 def __SetOS(self, os_resource): | |
423 """Indirect Accessor for _SetOS.""" | |
424 return self._SetOS(os_resource) | |
425 | |
426 os = property(__GetOS, __SetOS, | |
427 doc="""Gets and sets the resource to use as os.""") | |
428 | |
429 def _GetShutil(self): | |
430 """Accessor for shutil property.""" | |
431 return self.__shutil | |
432 | |
433 def __GetShutil(self): | |
434 """Indirect Accessor for _GetShutil.""" | |
435 return self._GetShutil() | |
436 | |
437 def _SetShutil(self, shutil_resource): | |
438 """Accessor for shutil property.""" | |
439 self.__shutil = shutil_resource | |
440 | |
441 def __SetShutil(self, shutil_resource): | |
442 """Indirect Accessor for _SetShutil.""" | |
443 return self._SetShutil(shutil_resource) | |
444 | |
445 shutil = property(__GetShutil, __SetShutil, | |
446 doc="""Gets and sets the resource to use as shutil.""") | |
447 | |
448 def _GetSys(self): | |
449 """Accessor for sys property.""" | |
450 return self.__sys | |
451 | |
452 def __GetSys(self): | |
453 """Indirect Accessor for _GetSys.""" | |
454 return self._GetSys() | |
455 | |
456 def _SetSys(self, sys_resource): | |
457 """Accessor for sys property.""" | |
458 self.__sys = sys_resource | |
459 | |
460 def __SetSys(self, sys_resource): | |
461 """Indirect Accessor for _SetSys.""" | |
462 return self._SetSys(sys_resource) | |
463 | |
464 sys = property(__GetSys, __SetSys, | |
465 doc="""Gets and sets the resource to use as sys.""") | |
466 | |
467 | |
468 def main(argv): | |
469 """Prepares the new project. | |
470 | |
471 Args: | |
472 argv: The arguments passed to the script by the shell. | |
473 """ | |
474 print 'init_project parsing its arguments.' | |
475 script_dir = os.path.abspath(os.path.dirname(__file__)) | |
476 options = ParseArguments(argv) | |
477 print 'init_project is preparing your project.' | |
478 # Check to see if the project is going into the SDK bundle. If so, issue a | |
479 # warning. | |
480 sdk_root_dir = os.getenv('NACL_SDK_ROOT', | |
481 os.path.dirname(os.path.dirname(script_dir))) | |
482 if sdk_root_dir: | |
483 if os.path.normpath(options.project_directory).count( | |
484 os.path.normpath(sdk_root_dir)) > 0: | |
485 print('WARNING: It looks like you are creating projects in the NaCl SDK ' | |
486 'directory %s.\nThese might be removed at the next update.' % | |
487 sdk_root_dir) | |
488 project_initializer = ProjectInitializer(options.is_c_project, | |
489 options.is_vs_project, | |
490 options.project_name, | |
491 options.project_directory, | |
492 options.nacl_platform, | |
493 script_dir, | |
494 nacl_sdk_root=sdk_root_dir) | |
495 project_initializer.PrepareDirectoryContent() | |
496 project_initializer.PrepareFileContent() | |
497 return 0 | |
498 | |
499 | |
500 if __name__ == '__main__': | |
501 try: | |
502 sys.exit(main(sys.argv[1:])) | |
503 except Exception as error: | |
504 print error | |
505 sys.exit(1) | |
OLD | NEW |