-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcreate_app.py
More file actions
executable file
·209 lines (178 loc) · 6.65 KB
/
create_app.py
File metadata and controls
executable file
·209 lines (178 loc) · 6.65 KB
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
#!/usr/bin/env python3
import re, subprocess, json, shutil
from typing import Dict, Optional, Pattern, Callable
from urllib.parse import urlparse
from datetime import datetime
from pathlib import Path
from template_engine import TemplateEngine
SCRIPT_PATH = Path(__file__).resolve()
SCRIPT_DIR = SCRIPT_PATH.parent
TEMPLATE_DIR = SCRIPT_DIR / "template"
TEMPLATE_CONFIG_PATH = SCRIPT_DIR / "TEMPLATE_CONFIG.json"
IGNORE_FILE_AND_DIRS = [
"node_modules",
"package-lock.json",
]
REGEXES = {
'No Slashes': re.compile(r'^[^/\\]+$'),
'File Path': re.compile("^(?![.])[A-Za-z0-9!#$%&()*+,\\-.:<=>?[\\]^_{}|~]+$"),
'App ID': re.compile('^(?:[a-zA-Z0-9_]+\\.){2,}[a-zA-Z0-9_-]+$'),
'Email': re.compile('^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$'),
'Yes or No or empty': re.compile('^[YyNn]?$'),
}
def get_input(
message: str,
*,
is_optional: bool = False,
err_message: str = 'Invalid input',
regex: Optional[Pattern[str]] = None,
test_func: Callable[[str], bool] = lambda _param: True,
) -> str:
message += ': '
while True:
response = input(message).strip()
if is_optional and response == '':
return ''
is_response_valid = (
test_func(response) # must pass test_func
and (not regex or bool(regex.fullmatch(response))) # must match regex if regex is supplied
and len(response) > 0 # must have at least one character
)
if is_response_valid:
return response
# response was not valid, print this error
print('. '.join((err_message, 'Please try again...')))
def validate_url(url: str) -> bool:
parsed = urlparse(url)
return parsed.scheme in ('http', 'https') and bool(parsed.netloc)
def ask_for_details() -> Dict[str, str]:
app_name = get_input('Enter app name (e.g: my-app)', regex=REGEXES['File Path'])
app_title = get_input(
'Enter app title (e.g: My App)',
err_message="Title must exist and cannot contain '/' or '\\'",
regex=REGEXES['No Slashes'],
)
app_id = get_input(
'Enter application ID (e.g: org.website.MyApp)',
err_message=(
'App ID must exist, contain 2 periods, be alphanumeric, cannot start nor end with a period,'
+ ' and can only contain hyphens (-) in the last part'
),
regex=REGEXES['App ID'],
)
git_repo = get_input(
'Enter Git repository URL (repo does not need to exist yet)',
err_message='Response must be a valid URL',
test_func=validate_url,
)
developer_name = get_input("Enter developer's name")
include_coc: bool = get_input(
'Does this project follow the GNOME Code of Conduct? [Y|n]',
regex=REGEXES['Yes or No or empty'],
is_optional=True,
) in ('Y', 'y', '')
if not include_coc:
IGNORE_FILE_AND_DIRS.append('CODE_OF_CONDUCT.md')
# Optionals
developer_email = get_input(
f"Enter developer's email address{'' if include_coc else ' (leave blank to ignore)'}",
is_optional=not include_coc,
err_message='Response must be a valid email address',
regex=REGEXES['Email'],
)
donation_link = get_input(
"Enter developer's donation link (leave blank to ignore)",
is_optional=True,
err_message='Response must be a valid URL',
test_func=validate_url,
)
return {
'APP_NAME': app_name,
'APP_TITLE': app_title,
'APP_ID': app_id,
'APP_ID_AS_PATH': app_id.replace('.', '/'),
'DEVELOPER_NAME': developer_name,
'DEVELOPER_EMAIL': developer_email,
'DONATION_LINK': donation_link,
'GIT_REPO': git_repo,
'INCLUDE_COC': 'yes' if include_coc else '',
'CURRENT_DATE_Y_m_d': datetime.now().strftime("%Y-%m-%d"),
}
def install_deps(config: Dict[str, str]):
print('Installing Flatpak dependencies...')
subprocess.run([
'flatpak', 'install',
'org.flatpak.Builder',
f"org.gnome.Sdk//{config['RUNTIME_VERSION']}",
f"org.gnome.Platform//{config['RUNTIME_VERSION']}",
f"org.freedesktop.Sdk.Extension.node{config['NODE_VERSION']}//{config['TS_NODE_RUNTIME_VERSION']}",
f"org.freedesktop.Sdk.Extension.typescript//{config['TS_NODE_RUNTIME_VERSION']}",
], check=True)
def git_setup(project_path: Path, config: Dict[str, str]):
if not project_path.is_dir():
raise ValueError(f"Project Path '{project_path.absolute()} is missing or is not a directory")
print('Initializing Git repo...')
subprocess.run(['git', 'init', '-b', 'main'], cwd=project_path, check=True)
print('Setting up submodules...')
subprocess.run([
'git', 'submodule', 'add',
'-b', f"sdk-v{config['RUNTIME_VERSION']}",
'https://github.com/flattool/gobjectify.git',
'src/gobjectify',
], cwd=project_path, check=True)
subprocess.run([
'git', 'submodule', 'add',
'-b', f"sdk-v{config['RUNTIME_VERSION']}",
'https://github.com/flattool/gir-ts-types.git',
'gi-types',
], cwd=project_path, check=True)
print('Adding initial commit...')
subprocess.run(['git', 'add', '.'], cwd=project_path, check=True)
subprocess.run([
'git', 'commit',
'-m', 'Initial commit: Flattool TypeScript App Template',
], cwd=project_path, check=True)
def ask_and_install_node_packages(project_path: Path):
if shutil.which("npm") == None:
print("'npm' not found, skipping the install of linting and formatting packages")
return
response = get_input(
'Install Node packages for formatting and linting? [Y|n]',
regex=REGEXES['Yes or No or empty'],
is_optional=True,
)
if response in ('Y', 'y', ''):
print('Installing Node packages for linting and formatting...')
subprocess.run(['npm', 'install'], cwd=project_path, check=True)
def main():
if not TEMPLATE_DIR.is_dir():
raise ValueError(f"{TEMPLATE_DIR} directory is missing or is not a directory")
if not TEMPLATE_CONFIG_PATH.is_file():
raise ValueError(f"{TEMPLATE_CONFIG_PATH} directory is missing or is not a file")
config = json.load(TEMPLATE_CONFIG_PATH.open('r'))
context = ask_for_details()
context.update(config)
new_project_path = SCRIPT_DIR.parent / context['APP_NAME']
if new_project_path.exists():
print(f"\nDirectory at '{new_project_path.absolute()}' already exists!")
return
engine = TemplateEngine()
engine.register_logic('ifset', lambda key, ctx: len(ctx.get(key, '')) > 0)
engine.register_logic('ifunset', lambda key, ctx: len(ctx.get(key, '')) == 0)
engine.render_files_recursive(TEMPLATE_DIR, new_project_path, context, ignore_paths=IGNORE_FILE_AND_DIRS)
subprocess.run(["chmod", "+x", f"{new_project_path}/run.sh"], check=True)
ask_and_install_node_packages(new_project_path)
install_deps(config)
git_setup(new_project_path, config)
print("Building initial build...")
subprocess.run([
'flatpak', 'run', 'org.flatpak.Builder',
'--user', '--force-clean', '_build',
f"build-aux/{context['APP_ID']}.json"
], cwd=new_project_path, check=True)
print("\n=== [ DONE ] ===")
print(f" Project created at: '{new_project_path}'")
print(' Make sure to setup proper metadata, README info, and desktop entry items!')
print("================")
if __name__ == '__main__':
main()