1# Copyright 2017 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Code that creates simple startup projects."""
16
17from pathlib import Path
18from enum import Enum
19import subprocess
20import shutil
21import sys
22import os
23import re
24from glob import glob
25from mesonbuild import mesonlib
26from mesonbuild.environment import detect_ninja
27from mesonbuild.templates.samplefactory import sameple_generator
28import typing as T
29
30if T.TYPE_CHECKING:
31    import argparse
32
33'''
34we currently have one meson template at this time.
35'''
36from mesonbuild.templates.mesontemplates import create_meson_build
37
38FORTRAN_SUFFIXES = {'.f', '.for', '.F', '.f90', '.F90'}
39LANG_SUFFIXES = {'.c', '.cc', '.cpp', '.cs', '.cu', '.d', '.m', '.mm', '.rs', '.java'} | FORTRAN_SUFFIXES
40LANG_SUPPORTED = {'c', 'cpp', 'cs', 'cuda', 'd', 'fortran', 'java', 'rust', 'objc', 'objcpp'}
41
42DEFAULT_PROJECT = 'executable'
43DEFAULT_VERSION = '0.1'
44class DEFAULT_TYPES(Enum):
45    EXE = 'executable'
46    LIB = 'library'
47
48INFO_MESSAGE = '''Sample project created. To build it run the
49following commands:
50
51meson setup builddir
52meson compile -C builddir
53'''
54
55
56def create_sample(options: 'argparse.Namespace') -> None:
57    '''
58    Based on what arguments are passed we check for a match in language
59    then check for project type and create new Meson samples project.
60    '''
61    sample_gen = sameple_generator(options)
62    if options.type == DEFAULT_TYPES['EXE'].value:
63        sample_gen.create_executable()
64    elif options.type == DEFAULT_TYPES['LIB'].value:
65        sample_gen.create_library()
66    else:
67        raise RuntimeError('Unreachable code')
68    print(INFO_MESSAGE)
69
70def autodetect_options(options: 'argparse.Namespace', sample: bool = False) -> None:
71    '''
72    Here we autodetect options for args not passed in so don't have to
73    think about it.
74    '''
75    if not options.name:
76        options.name = Path().resolve().stem
77        if not re.match('[a-zA-Z_][a-zA-Z0-9]*', options.name) and sample:
78            raise SystemExit(f'Name of current directory "{options.name}" is not usable as a sample project name.\n'
79                             'Specify a project name with --name.')
80        print(f'Using "{options.name}" (name of current directory) as project name.')
81    if not options.executable:
82        options.executable = options.name
83        print(f'Using "{options.executable}" (project name) as name of executable to build.')
84    if sample:
85        # The rest of the autodetection is not applicable to generating sample projects.
86        return
87    if not options.srcfiles:
88        srcfiles = []
89        for f in (f for f in Path().iterdir() if f.is_file()):
90            if f.suffix in LANG_SUFFIXES:
91                srcfiles.append(f)
92        if not srcfiles:
93            raise SystemExit('No recognizable source files found.\n'
94                             'Run meson init in an empty directory to create a sample project.')
95        options.srcfiles = srcfiles
96        print("Detected source files: " + ' '.join(map(str, srcfiles)))
97    options.srcfiles = [Path(f) for f in options.srcfiles]
98    if not options.language:
99        for f in options.srcfiles:
100            if f.suffix == '.c':
101                options.language = 'c'
102                break
103            if f.suffix in ('.cc', '.cpp'):
104                options.language = 'cpp'
105                break
106            if f.suffix == '.cs':
107                options.language = 'cs'
108                break
109            if f.suffix == '.cu':
110                options.language = 'cuda'
111                break
112            if f.suffix == '.d':
113                options.language = 'd'
114                break
115            if f.suffix in FORTRAN_SUFFIXES:
116                options.language = 'fortran'
117                break
118            if f.suffix == '.rs':
119                options.language = 'rust'
120                break
121            if f.suffix == '.m':
122                options.language = 'objc'
123                break
124            if f.suffix == '.mm':
125                options.language = 'objcpp'
126                break
127            if f.suffix == '.java':
128                options.language = 'java'
129                break
130        if not options.language:
131            raise SystemExit("Can't autodetect language, please specify it with -l.")
132        print("Detected language: " + options.language)
133
134def add_arguments(parser: 'argparse.ArgumentParser') -> None:
135    '''
136    Here we add args for that the user can passed when making a new
137    Meson project.
138    '''
139    parser.add_argument("srcfiles", metavar="sourcefile", nargs="*", help="source files. default: all recognized files in current directory")
140    parser.add_argument('-C', dest='wd', action=mesonlib.RealPathAction,
141                        help='directory to cd into before running')
142    parser.add_argument("-n", "--name", help="project name. default: name of current directory")
143    parser.add_argument("-e", "--executable", help="executable name. default: project name")
144    parser.add_argument("-d", "--deps", help="dependencies, comma-separated")
145    parser.add_argument("-l", "--language", choices=sorted(LANG_SUPPORTED), help="project language. default: autodetected based on source files")
146    parser.add_argument("-b", "--build", action='store_true', help="build after generation")
147    parser.add_argument("--builddir", default='build', help="directory for build")
148    parser.add_argument("-f", "--force", action="store_true", help="force overwrite of existing files and directories.")
149    parser.add_argument('--type', default=DEFAULT_PROJECT, choices=('executable', 'library'), help=f"project type. default: {DEFAULT_PROJECT} based project")
150    parser.add_argument('--version', default=DEFAULT_VERSION, help=f"project version. default: {DEFAULT_VERSION}")
151
152def run(options: 'argparse.Namespace') -> int:
153    '''
154    Here we generate the new Meson sample project.
155    '''
156    if not Path(options.wd).exists():
157        sys.exit('Project source root directory not found. Run this command in source directory root.')
158    os.chdir(options.wd)
159
160    if not glob('*'):
161        autodetect_options(options, sample=True)
162        if not options.language:
163            print('Defaulting to generating a C language project.')
164            options.language = 'c'
165        create_sample(options)
166    else:
167        autodetect_options(options)
168        if Path('meson.build').is_file() and not options.force:
169            raise SystemExit('meson.build already exists. Use --force to overwrite.')
170        create_meson_build(options)
171    if options.build:
172        if Path(options.builddir).is_dir() and options.force:
173            print('Build directory already exists, deleting it.')
174            shutil.rmtree(options.builddir)
175        print('Building...')
176        cmd = mesonlib.get_meson_command() + [options.builddir]
177        ret = subprocess.run(cmd)
178        if ret.returncode:
179            raise SystemExit
180        cmd = detect_ninja() + ['-C', options.builddir]
181        ret = subprocess.run(cmd)
182        if ret.returncode:
183            raise SystemExit
184    return 0
185