v / cmd / tools / amalgamate.v
221 lines · 182 sloc · 5.8 KB · b801083f13615bfcebe4dd3c25fdfc031d9eb268
Raw
1// amalgamate multiple C source files into a single
2// C source file. See https://sqlite.org/amalgamation.html
3// for a description of file amalgamation.
4//
5// If an input file is not specified, source is read
6// from stdin.
7//
8// If an output file is not specified, source is output
9// to stdout.
10
11module main
12
13import flag
14import os
15import regex
16
17const app_name = 'amalgamate'
18const app_version = '0.0.1'
19
20// pre-compile the include statement regex
21const re = regex.regex_opt(r'^\s*#\s*include\s*"([^"]+)"')!
22
23struct Config {
24mut:
25 input_files []string
26 output_file string
27 search_dirs []string
28 blacklist []string
29}
30
31struct Context {
32 config Config
33mut:
34 processed_files []string
35}
36
37fn parse_arguments() Config {
38 mut cfg := Config{}
39
40 mut parser := flag.new_flag_parser(os.args)
41 parser.skip_executable()
42 parser.application(app_name)
43 parser.version(app_version)
44
45 parser.arguments_description('[file ...]')
46
47 parser.description('combine multiple .c and .h files into one.')
48 parser.description('')
49 parser.description('Combine input, coming from either stdin or input files, into one')
50 parser.description('large file. Include statements are processed and the contents')
51 parser.description('copied in place. Only #include "file.h" statements cause their')
52 parser.description('contents to be copied, not #include <file.h> statements. If no')
53 parser.description('input files are specified, read from stdin.')
54
55 parser.footer('\nAn example showing multiple blacklisted files and multiple search')
56 parser.footer('directories.')
57 parser.footer('')
58 parser.footer(' amalgamate -o output_file.c -b ignore_me.h \\')
59 parser.footer(' -b ignore_me_2.h -b other/ignore_me.h \\')
60 parser.footer(' -s relative/search/dir -s /absolute/search/dir \\')
61 parser.footer(' file1.c file2.c')
62 parser.footer('')
63
64 cfg.output_file = parser.string('output', `o`, '', 'output file. If not specified,\n' +
65 flag.space + 'defaults to stdout.\n', val_desc: '<filename>')
66
67 cfg.blacklist = parser.string_multi('blacklist', `b`,
68 'blacklist a file name. This prevents\n' + flag.space +
69 'the named file from being included.\n' + flag.space +
70 'This can be specified more that once.\n', val_desc: '<include_file>')
71
72 cfg.search_dirs = parser.string_multi('search_path', `s`,
73 'add a directory to the search path.\n' + flag.space +
74 'An include file is searched for in\n' + flag.space +
75 'the current working directory and\n' + flag.space +
76 'if not found, the directories in this\n' + flag.space +
77 'list are searched, in order, until the\n' + flag.space +
78 'file is found or the search list is\n' + flag.space +
79 'exhausted. This can be specified\n' + flag.space + 'more that once.\n',
80 val_desc: '<search_dir>'
81 )
82
83 cfg.input_files = parser.finalize() or {
84 // this only reports the first unrecognized argument
85 eprintln('${err}\n')
86 eprintln('${parser.usage()}\n')
87 exit(1)
88 }
89
90 return cfg
91}
92
93fn main() {
94 cfg := parse_arguments()
95
96 mut ctx := Context{
97 config: cfg
98 }
99
100 ctx.amalgamate() or {
101 eprintln('error: ${err}')
102 exit(1)
103 }
104}
105
106fn (mut c Context) amalgamate() ! {
107 mut source := ''
108
109 if c.config.input_files.len == 0 {
110 // source += '/* ########## stdin */\n'
111 // if there are no input files, read from stdin
112 local_dir := os.getwd()
113 source += c.handle_includes(local_dir, os.get_raw_lines_joined())!
114 // source += '/* ########## stdin end */\n'
115 } else {
116 // read each input file, in order, and
117 // handle all of its includes.
118 for file in c.config.input_files {
119 if file in c.config.blacklist {
120 // skip blacklisted files
121 continue
122 }
123
124 found_file := c.find_file(file)!
125
126 if found_file in c.processed_files {
127 // skip over files already read
128 continue
129 }
130
131 // source += '/* ########## ${file} */\n'
132 c.processed_files << found_file
133 local_dir := os.dir(found_file)
134 file_source_code := os.read_file(found_file)!
135 source += c.handle_includes(local_dir, file_source_code)!
136 // source += '/* ########## ${file} end */\n'
137 }
138 }
139
140 if c.config.output_file == '' {
141 print(source)
142 } else {
143 os.write_file(c.config.output_file, source)!
144 }
145
146 return
147}
148
149fn (c Context) find_file(file string) !string {
150 mut full_path := os.real_path(file)
151
152 if os.is_file(full_path) {
153 return full_path
154 }
155
156 for dir in c.config.search_dirs {
157 full_path = os.real_path(os.join_path_single(dir, file))
158
159 if os.is_file(full_path) {
160 return full_path
161 }
162 }
163
164 return error('file "${file}" not found')
165}
166
167// handle_includes looks for lines that start with #include
168// and inserts the lines from the named include file.
169//
170// The pattern matches file names for local header files,
171// not system header files as are denoted by < and >.
172fn (mut c Context) handle_includes(local_dir string, input_source string) !string {
173 source_lines := input_source.split_into_lines()
174 mut output_lines := []string{}
175
176 for line in source_lines {
177 start, _ := re.match_string(line)
178
179 if start >= 0 {
180 file := line[re.groups[0]..re.groups[1]]
181 mut found_file := ''
182
183 if file in c.config.blacklist {
184 // leave blacklisted files alone
185 if file in c.processed_files {
186 // we don't want a second include
187 output_lines << '\n'
188 } else {
189 output_lines << line
190 c.processed_files << file
191 }
192 continue
193 }
194
195 if !os.is_abs_path(file) {
196 found_file = c.find_file(os.join_path_single(local_dir, file)) or {
197 // keep looking
198 ''
199 }
200 }
201
202 if found_file == '' {
203 found_file = c.find_file(file)!
204 }
205
206 if found_file in c.processed_files {
207 // skip over files already read
208 continue
209 }
210 c.processed_files << found_file
211 file_source_code := os.read_file(found_file)!
212 // output_lines << '/* ########## ${file} begin */\n'
213 output_lines << c.handle_includes(os.dir(found_file), file_source_code)!
214 // output_lines << '/* ########## ${file} end */\n'
215 } else {
216 output_lines << line
217 }
218 }
219
220 return output_lines.join_lines() + '\n'
221}
222