summaryrefslogtreecommitdiffstats
path: root/native_client_sdk/src/project_templates/init_project.py
blob: 12dd3c787afb0e6a1463a1dd62d569e103d827a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
#!/usr/bin/env python
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""A simple project generator for Native Client projects written in C or C++.

This script accepts a few argument which it uses as a description of a new NaCl
project.  It sets up a project with a given name and a given primary language
(default: C++, optionally, C) using the appropriate files from this area.
This script does not handle setup for complex applications, just the basic
necessities to get a functional native client application stub.  When this
script terminates a compileable project stub will exist with the specified
name, at the specified location.

GetCamelCaseName(): Converts an underscore name to a camel case name.
GetCodeDirectory(): Decides what directory to pull source code from.
GetCodeSoureFiles(): Decides what source files to pull into the stub.
GetCommonSourceFiles(): Gives list of files needed by all project types.
GetHTMLDirectory(): Decides what directory to pull HTML stub from.
GetHTMLSourceFiles(): Gives HTML files to be included in project stub.
GetTargetFileName(): Converts a source file name into a project file name.
ParseArguments(): Parses the arguments provided by the user.
ReplaceInFile(): Replaces a given string with another in a given file.
ProjectInitializer: Maintains some state applicable to setting up a project.
main(): Executes the script.
"""

__author__ = 'mlinck@google.com (Michael Linck)'

import fileinput
import optparse
import os.path
import shutil
import sys
import uuid

# A list of all platforms that should have make.cmd.
WINDOWS_BUILD_PLATFORMS = ['cygwin', 'win32']

# Tags that will be replaced in our the new project's source files.
PROJECT_NAME_TAG = '<PROJECT_NAME>'
PROJECT_NAME_CAMEL_CASE_TAG = '<ProjectName>'
SDK_ROOT_TAG = '<NACL_SDK_ROOT>'
NACL_PLATFORM_TAG = '<NACL_PLATFORM>'
VS_PROJECT_UUID_TAG = '<VS_PROJECT_UUID>'
VS_SOURCE_UUID_TAG = '<VS_SOURCE_UUID>'
VS_HEADER_UUID_TAG = '<VS_HEADER_UUID>'
VS_RESOURCE_UUID_TAG = '<VS_RESOURCE_UUID>'

# This string is the part of the file name that will be replaced.
PROJECT_FILE_NAME = 'project_file'

# Lists of source files that will be used for the new project.
COMMON_PROJECT_FILES = ['scons']
C_SOURCE_FILES = ['build.scons', '%s.c' % PROJECT_FILE_NAME]
CC_SOURCE_FILES = ['build.scons', '%s.cc' % PROJECT_FILE_NAME]
HTML_FILES = ['%s.html' % PROJECT_FILE_NAME]
VS_FILES = ['%s.sln' % PROJECT_FILE_NAME, '%s.vcproj' % PROJECT_FILE_NAME]

# Error needs to be a class, since we 'raise' it in several places.
class Error(Exception):
  pass


def GetCamelCaseName(lower_case_name):
  """Converts an underscore name to a camel case name.

  Args:
    lower_case_name: The name in underscore-delimited lower case format.

  Returns:
    The name in camel case format.
  """
  camel_case_name = ''
  name_parts = lower_case_name.split('_')
  for part in name_parts:
    if part:
      camel_case_name += part.capitalize()
  return camel_case_name


def GetCodeDirectory(is_c_project, project_templates_dir):
  """Decides what directory to pull source code from.

  Args:
    is_c_project: A boolean indicating whether this project is in C or not.
    project_templates_dir: The path to the project_templates directory.

  Returns:
    The code directory for the given project type.
  """
  stub_directory = ''
  if is_c_project:
    stub_directory = os.path.join(project_templates_dir, 'c')
  else:
    stub_directory = os.path.join(project_templates_dir, 'cc')
  return stub_directory


def GetCodeSourceFiles(is_c_project):
  """Decides what source files to pull into the stub.

  Args:
    is_c_project: A boolean indicating whether this project is in C or not.

  Returns:
    The files that are specific to the requested type of project and live in its
    directory.
  """
  project_files = []
  if is_c_project:
    project_files = C_SOURCE_FILES
  else:
    project_files = CC_SOURCE_FILES
  return project_files


def GetCommonSourceFiles():
  """Gives list of files needed by all project types.

  Returns:
    The files C and C++ projects have in common.  These are the files that live
    in the top level project_templates directory.
  """
  project_files = COMMON_PROJECT_FILES
  if sys.platform in WINDOWS_BUILD_PLATFORMS:
    project_files.extend(['scons.bat'])
  return project_files


def GetVsDirectory(project_templates_dir):
  """Decides what directory to pull Visual Studio stub from.

  Args:
    project_templates_dir: The path to the project_templates directory.

  Returns:
    The directory where the HTML stub is to be found.
  """
  return os.path.join(project_templates_dir, 'vs')


def GetVsProjectFiles():
  """Gives VisualStudio files to be included in project stub.

  Returns:
    The VisualStudio files needed for the project.
  """
  return VS_FILES


def GetHTMLDirectory(project_templates_dir):
  """Decides what directory to pull HTML stub from.

  Args:
    project_templates_dir: The path to the project_templates directory.

  Returns:
    The directory where the HTML stub is to be found.
  """
  return os.path.join(project_templates_dir, 'html')


def GetHTMLSourceFiles():
  """Gives HTML files to be included in project stub.

  Returns:
    The HTML files needed for the project.
  """
  return HTML_FILES


def GetTargetFileName(source_file_name, project_name):
  """Converts a source file name into a project file name.

  Args:
    source_file_name: The name of a file that is to be included in the project
        stub, as it appears at the source location.
    project_name: The name of the project that is being generated.

  Returns:
    The target file name for a given source file.  All project files are run
    through this filter and it modifies them as needed.
  """
  target_file_name = ''
  if source_file_name.startswith(PROJECT_FILE_NAME):
    target_file_name = source_file_name.replace(PROJECT_FILE_NAME,
                                                project_name)
  else:
    target_file_name = source_file_name
  return target_file_name


def GetDefaultProjectDir():
  """Determines the default project directory.

  The default directory root for new projects is called 'nacl_projects' under
  the user's home directory.  There are two ways to override this: you can set
  the NACL_PROJECT_ROOT environment variable, or use the --directory option.

  Returns:
    An os-specific path to the default project directory, which is called
    'nacl_projects' under the user's home directory.
  """
  return os.getenv('NACL_PROJECT_ROOT',
                   os.path.join(os.path.expanduser('~'), 'nacl_projects'))


def ParseArguments(argv):
  """Parses the arguments provided by the user.

  Parses the command line options and makes sure the script errors when it is
  supposed to.

  Args:
    argv: The argument array.

  Returns:
    The options structure that represents the arguments after they have been
    parsed.
  """
  parser = optparse.OptionParser()
  parser.add_option(
      '-n', '--name', dest='project_name',
      default='',
      help=('Required: the name of the new project to be stubbed out.\n'
            'Please use lower case names with underscore, i.e. hello_world.'))
  parser.add_option(
      '-d', '--directory', dest='project_directory',
      default=GetDefaultProjectDir(),
      help=('Optional: If set, the new project will be created under this '
            'directory and the directory created if necessary.'))
  parser.add_option(
      '-c', action='store_true', dest='is_c_project',
      default=False,
      help=('Optional: If set, this will generate a C project.  Default '
            'is C++.'))
  parser.add_option(
      '-p', '--nacl-platform', dest='nacl_platform',
      default='pepper_17',
      help=('Optional: if set, the new project will target the given nacl\n'
            'platform. Default is the most current platform. e.g. pepper_17'))
  parser.add_option(
      '--vsproj', action='store_true', dest='is_vs_project',
      default=False,
      help=('Optional: If set, generate Visual Studio project files.'))
  result = parser.parse_args(argv)
  options = result[0]
  args = result[1]
  #options, args) = parser.parse_args(argv)
  if args:
    parser.print_help()
    sys.exit(1)
  elif not options.project_name.islower():
    print('--name missing or in incorrect format.  Please use -h for '
          'instructions.')
    sys.exit(1)
  return options


class ProjectInitializer(object):
  """Maintains the state of the project that is being created."""

  def __init__(self, is_c_project, is_vs_project, project_name,
               project_location, nacl_platform, project_templates_dir,
               nacl_sdk_root=None, os_resource=os):
    """Initializes all the fields that are known after parsing the parameters.

    Args:
      is_c_project: A boolean indicating whether this project is in C or not.
      is_vs_project: A boolean indicating whether this project has Visual
        Studio support.
      project_name: A string containing the name of the project to be created.
      project_location: A path indicating where the new project is to be placed.
      project_templates_dir: The path to the project_templates directory.
      os_resource: A resource to be used as os.  Provided for unit testing.
    """
    self.__is_c_project = is_c_project
    self.__is_vs_project = is_vs_project
    self.__project_files = []
    self.__project_name = project_name
    self.__project_location = project_location
    self.__nacl_platform = nacl_platform
    self.__project_templates_dir = project_templates_dir
    # System resources are properties so mocks can be inserted.
    self.__fileinput = fileinput
    self.__nacl_sdk_root = nacl_sdk_root
    self.__os = os_resource
    self.__shutil = shutil
    self.__sys = sys
    self.__CreateProjectDirectory()

  def CopyAndRenameFiles(self, source_dir, file_names):
    """Places files in the new project's directory and renames them as needed.

    Copies the given files from the given source directory into the new
    project's directory, renaming them as necessary.  Each file that is created
    in the project directory is also added to self.__project_files.

    Args:
      source_dir: A path indicating where the files are to be copied from.
      file_names: The list of files that is to be copied out of source_dir.
    """
    for source_file_name in file_names:
      target_file_name = GetTargetFileName(source_file_name,
                                           self.__project_name)
      copy_source_file = self.os.path.join(source_dir, source_file_name)
      copy_target_file = self.os.path.join(self.__project_dir, target_file_name)
      self.shutil.copy(copy_source_file, copy_target_file)
      self.__project_files += [copy_target_file]

  def __CreateProjectDirectory(self):
    """Creates the project's directory and any parents as necessary."""
    self.__project_dir = self.os.path.join(self.__project_location,
                                           self.__project_name)
    if self.os.path.exists(self.__project_dir):
      raise Error("Error: directory '%s' already exists" % self.__project_dir)
    self.os.makedirs(self.__project_dir)

  def PrepareDirectoryContent(self):
    """Prepares the directory for the new project.

    This function's job is to know what directories need to be used and what
    files need to be copied and renamed.  It uses several tiny helper functions
    to do this.
    There are three locations from which files are copied to create a project.
    That number may change in the future.
    """
    code_source_dir = GetCodeDirectory(self.__is_c_project,
                                       self.__project_templates_dir)
    code_source_files = GetCodeSourceFiles(self.__is_c_project)
    html_source_dir = GetHTMLDirectory(self.__project_templates_dir)
    html_source_files = GetHTMLSourceFiles()
    common_source_files = GetCommonSourceFiles()
    self.CopyAndRenameFiles(code_source_dir, code_source_files)
    self.CopyAndRenameFiles(html_source_dir, html_source_files)
    self.CopyAndRenameFiles(self.__project_templates_dir,
                            common_source_files)
    if  self.__is_vs_project:
      vs_source_dir = GetVsDirectory(self.__project_templates_dir)
      vs_files = GetVsProjectFiles()
      self.CopyAndRenameFiles(vs_source_dir, vs_files)
    print('init_project has copied the appropriate files to: %s' %
          self.__project_dir)

  def PrepareFileContent(self):
    """Changes contents of files in the new project as needed.

    Goes through each file in the project that is being created and replaces
    contents as necessary.
    """
    camel_case_name = GetCamelCaseName(self.__project_name)
    sdk_root_dir = self.__nacl_sdk_root
    if not sdk_root_dir:
      raise Error("Error: NACL_SDK_ROOT is not set")
    sdk_root_dir = self.os.path.abspath(sdk_root_dir)
    if  self.__is_vs_project:
      project_uuid = str(uuid.uuid4()).upper()
      vs_source_uuid = str(uuid.uuid4()).upper()
      vs_header_uuid = str(uuid.uuid4()).upper()
      vs_resource_uuid = str(uuid.uuid4()).upper()
    for project_file in self.__project_files:
      self.ReplaceInFile(project_file, PROJECT_NAME_TAG, self.__project_name)
      self.ReplaceInFile(project_file,
                         PROJECT_NAME_CAMEL_CASE_TAG,
                         camel_case_name)
      self.ReplaceInFile(project_file, SDK_ROOT_TAG, sdk_root_dir)
      self.ReplaceInFile(project_file, NACL_PLATFORM_TAG, self.__nacl_platform)
      if  self.__is_vs_project:
        self.ReplaceInFile(project_file, VS_PROJECT_UUID_TAG, project_uuid)
        self.ReplaceInFile(project_file, VS_SOURCE_UUID_TAG, vs_source_uuid)
        self.ReplaceInFile(project_file, VS_HEADER_UUID_TAG, vs_header_uuid)
        self.ReplaceInFile(project_file, VS_RESOURCE_UUID_TAG, vs_resource_uuid)

  def ReplaceInFile(self, file_path, old_text, new_text):
    """Replaces a given string with another in a given file.

    Args:
      file_path: The path to the file that is to be modified.
      old_text: The text that is to be removed.
      new_text: The text that is to be added in place of old_text.
    """
    for line in self.fileinput.input(file_path, inplace=1, mode='U'):
      self.sys.stdout.write(line.replace(old_text, new_text))

  # The following properties exist to make unit testing possible.

  def _GetFileinput(self):
    """Accessor for Fileinput property."""
    return self.__fileinput

  def __GetFileinput(self):
    """Indirect Accessor for _GetFileinput."""
    return self._GetFileinput()

  def _SetFileinput(self, fileinput_resource):
    """Accessor for Fileinput property."""
    self.__fileinput = fileinput_resource

  def __SetFileinput(self, fileinput_resource):
    """Indirect Accessor for _SetFileinput."""
    return self._SetFileinput(fileinput_resource)

  fileinput = property(
      __GetFileinput, __SetFileinput,
      doc="""Gets and sets the resource to use as fileinput.""")

  def _GetOS(self):
    """Accessor for os property."""
    return self.__os

  def __GetOS(self):
    """Indirect Accessor for _GetOS."""
    return self._GetOS()

  def _SetOS(self, os_resource):
    """Accessor for os property."""
    self.__os = os_resource

  def __SetOS(self, os_resource):
    """Indirect Accessor for _SetOS."""
    return self._SetOS(os_resource)

  os = property(__GetOS, __SetOS,
                doc="""Gets and sets the resource to use as os.""")

  def _GetShutil(self):
    """Accessor for shutil property."""
    return self.__shutil

  def __GetShutil(self):
    """Indirect Accessor for _GetShutil."""
    return self._GetShutil()

  def _SetShutil(self, shutil_resource):
    """Accessor for shutil property."""
    self.__shutil = shutil_resource

  def __SetShutil(self, shutil_resource):
    """Indirect Accessor for _SetShutil."""
    return self._SetShutil(shutil_resource)

  shutil = property(__GetShutil, __SetShutil,
                    doc="""Gets and sets the resource to use as shutil.""")

  def _GetSys(self):
    """Accessor for sys property."""
    return self.__sys

  def __GetSys(self):
    """Indirect Accessor for _GetSys."""
    return self._GetSys()

  def _SetSys(self, sys_resource):
    """Accessor for sys property."""
    self.__sys = sys_resource

  def __SetSys(self, sys_resource):
    """Indirect Accessor for _SetSys."""
    return self._SetSys(sys_resource)

  sys = property(__GetSys, __SetSys,
                 doc="""Gets and sets the resource to use as sys.""")


def main(argv):
  """Prepares the new project.

  Args:
    argv: The arguments passed to the script by the shell.
  """
  print 'init_project parsing its arguments.'
  script_dir = os.path.abspath(os.path.dirname(__file__))
  options = ParseArguments(argv)
  print 'init_project is preparing your project.'
  # Check to see if the project is going into the SDK bundle.  If so, issue a
  # warning.
  sdk_root_dir = os.getenv('NACL_SDK_ROOT',
                           os.path.dirname(os.path.dirname(script_dir)))
  if sdk_root_dir:
    if os.path.normpath(options.project_directory).count(
        os.path.normpath(sdk_root_dir)) > 0:
      print('WARNING: It looks like you are creating projects in the NaCl SDK '
            'directory %s.\nThese might be removed at the next update.' %
            sdk_root_dir)
  project_initializer = ProjectInitializer(options.is_c_project,
                                           options.is_vs_project,
                                           options.project_name,
                                           options.project_directory,
                                           options.nacl_platform,
                                           script_dir,
                                           nacl_sdk_root=sdk_root_dir)
  project_initializer.PrepareDirectoryContent()
  project_initializer.PrepareFileContent()
  return 0


if __name__ == '__main__':
  try:
    sys.exit(main(sys.argv[1:]))
  except Exception as error:
    print error
    sys.exit(1)