1 module digger;
2 
3 import std.array;
4 import std.exception;
5 import std.file : thisExePath, exists;
6 import std.path;
7 import std.process;
8 import std.stdio;
9 import std.typetuple;
10 
11 static if(!is(typeof({import ae.utils.text;}))) static assert(false, "ae library not found, did you clone with --recursive?"); else:
12 
13 version (Windows)
14 	static import ae.sys.net.wininet;
15 else
16 	static import ae.sys.net.curl;
17 
18 import ae.sys.d.manager : DManager;
19 import ae.utils.funopt;
20 import ae.utils.main;
21 import ae.utils.meta : structFun;
22 import ae.utils.text : eatLine;
23 
24 import bisect;
25 import common;
26 import config;
27 import custom;
28 import install;
29 import repo;
30 
31 // http://d.puremagic.com/issues/show_bug.cgi?id=7016
32 version(Windows) static import ae.sys.windows;
33 
34 alias BuildOptions(string action, string pastAction, bool showBuildActions = true) = TypeTuple!(
35 	Switch!(hiddenOption, 0, "64"),
36 	Option!(string, showBuildActions ? "Select models (32, 64, or, on Windows, 32mscoff). You can specify multiple models by comma-separating them.\nOn this system, the default is " ~ DManager.Config.Build.components.common.defaultModel ~ " [build.components.common.models]" : hiddenOption, null, 0, "model"),
37 	Option!(string[], "Do not " ~ action ~ " a component (that would otherwise be " ~ pastAction ~ " by default). List of default components: " ~ DManager.defaultComponents.join(", ") ~ " [build.components.enable.COMPONENT=false]", "COMPONENT", 0, "without"),
38 	Option!(string[], "Specify an additional D component to " ~ action ~ ". List of available additional components: " ~ DManager.additionalComponents.join(", ") ~ " [build.components.enable.COMPONENT=true]", "COMPONENT", 0, "with"),
39 	Option!(string[], showBuildActions ? `Additional make parameters, e.g. "HOST_CC=g++48" [build.components.common.makeArgs]` : hiddenOption, "ARG", 0, "makeArgs"),
40 	Switch!(showBuildActions ? "Bootstrap the compiler (build from C++ source code) instead of downloading a pre-built binary package [build.components.dmd.bootstrap.fromSource]" : hiddenOption, 0, "bootstrap"),
41 	Switch!(hiddenOption, 0, "use-vc"),
42 	Switch!(hiddenOption, 0, "clobber-local-changes"),
43 );
44 
45 enum specDescription = "D ref (branch / tag / point in time) to build, plus any additional forks or pull requests. Example:\n" ~
46 	"\"master @ 3 weeks ago + dmd#123 + You/dmd/awesome-feature\"";
47 
48 void parseBuildOptions(T...)(T options) // T == BuildOptions!action
49 {
50 	if (options[0])
51 		d.config.build.components.common.models = ["64"];
52 	if (options[1])
53 		d.config.build.components.common.models = options[1].value.split(",");
54 	foreach (componentName; options[2])
55 		d.config.build.components.enable[componentName] = false;
56 	foreach (componentName; options[3])
57 		d.config.build.components.enable[componentName] = true;
58 	d.config.build.components.common.makeArgs ~= options[4];
59 	d.config.build.components.dmd.bootstrap.fromSource |= options[5];
60 	d.config.build.components.dmd.useVC |= options[6];
61 	d.verifyWorkTree = !options[7];
62 	static assert(options.length == 8);
63 }
64 
65 struct Digger
66 {
67 static:
68 	@(`Build D from source code`)
69 	int build(BuildOptions!("build", "built") options, Parameter!(string, specDescription) spec = "master")
70 	{
71 		parseBuildOptions(options);
72 		buildCustom(spec);
73 		return 0;
74 	}
75 
76 	@(`Incrementally rebuild the current D checkout`)
77 	int rebuild(BuildOptions!("rebuild", "rebuilt") options)
78 	{
79 		parseBuildOptions(options);
80 		incrementalBuild();
81 		return 0;
82 	}
83 
84 	@(`Run tests for enabled components`)
85 	int test(BuildOptions!("test", "tested") options)
86 	{
87 		parseBuildOptions(options);
88 		runTests();
89 		return 0;
90 	}
91 
92 	@(`Check out D source code from git`)
93 	int checkout(BuildOptions!("check out", "checked out", false) options, Parameter!(string, specDescription) spec = "master")
94 	{
95 		parseBuildOptions(options);
96 		.checkout(spec);
97 		return 0;
98 	}
99 
100 	@(`Run a command using a D version`)
101 	int run(
102 		BuildOptions!("build", "built") options,
103 		Parameter!(string, specDescription ~ "\nSpecify \"-\" to use the previously-built version.") spec,
104 		Parameter!(string[], "Command to run and its arguments (use -- to pass switches)") command)
105 	{
106 		if (spec == "-")
107 			enforce(options == typeof(options).init, "Can't specify build options when using the last built version.");
108 		else
109 		{
110 			parseBuildOptions(options);
111 			buildCustom(spec, /*asNeeded*/true);
112 		}
113 
114 		auto binPath = resultDir.buildPath("bin").absolutePath();
115 		environment["PATH"] = binPath ~ pathSeparator ~ environment["PATH"];
116 
117 		version (Windows)
118 			return spawnProcess(command).wait();
119 		else
120 		{
121 			execvp(command[0], command);
122 			errnoEnforce(false, "execvp failed");
123 			assert(false); // unreachable
124 		}
125 	}
126 
127 	@(`Install Digger's build result on top of an existing stable DMD installation`)
128 	int install(
129 		Switch!("Do not prompt", 'y') yes,
130 		Switch!("Only print what would be done", 'n') dryRun,
131 		Parameter!(string, "Directory to install to. Default is to find one in PATH.") installLocation = null,
132 	)
133 	{
134 		enforce(!yes || !dryRun, "--yes and --dry-run are mutually exclusive");
135 		.install.install(yes, dryRun, installLocation);
136 		return 0;
137 	}
138 
139 	@(`Undo the "install" action`)
140 	int uninstall(
141 		Switch!("Only print what would be done", 'n') dryRun,
142 		Switch!("Do not verify files to be deleted; ignore errors") force,
143 		Parameter!(string, "Directory to uninstall from. Default is to search PATH.") installLocation = null,
144 	)
145 	{
146 		.uninstall(dryRun, force, installLocation);
147 		return 0;
148 	}
149 
150 	@(`Bisect D history according to a bisect.ini file`)
151 	int bisect(
152 		Switch!("Skip sanity-check of the GOOD/BAD commits.") noVerify,
153 		Option!(string[], "Additional bisect configuration. Equivalent to bisect.ini settings.", "NAME=VALUE", 'c', "config") configLines,
154 		Parameter!(string, "Location of the bisect.ini file containing the bisection description.") bisectConfigFile = null,
155 	)
156 	{
157 		return doBisect(noVerify, bisectConfigFile, configLines);
158 	}
159 
160 	@(`Cache maintenance actions (run with no arguments for details)`)
161 	int cache(string[] args)
162 	{
163 		static struct CacheActions
164 		{
165 		static:
166 			@(`Compact the cache`)
167 			int compact()
168 			{
169 				d.optimizeCache();
170 				return 0;
171 			}
172 
173 			@(`Delete entries cached as unbuildable`)
174 			int purgeUnbuildable()
175 			{
176 				d.purgeUnbuildable();
177 				return 0;
178 			}
179 
180 			@(`Migrate cached entries from one cache engine to another`)
181 			int migrate(string source, string target)
182 			{
183 				d.migrateCache(source, target);
184 				return 0;
185 			}
186 		}
187 
188 		return funoptDispatch!CacheActions(["digger cache"] ~ args);
189 	}
190 
191 	// hidden actions
192 
193 	int buildAll(BuildOptions!("build", "built") options, string spec = "master")
194 	{
195 		parseBuildOptions(options);
196 		.buildAll(spec);
197 		return 0;
198 	}
199 
200 	int delve(bool inBisect)
201 	{
202 		return doDelve(inBisect);
203 	}
204 
205 	int parseRev(string rev)
206 	{
207 		stdout.writeln(.parseRev(rev));
208 		return 0;
209 	}
210 
211 	int show(string revision)
212 	{
213 		d.getMetaRepo().git.run("log", "-n1", revision);
214 		d.getMetaRepo().git.run("log", "-n1", "--pretty=format:t=%ct", revision);
215 		return 0;
216 	}
217 
218 	int getLatest()
219 	{
220 		writeln((cast(DManager.Website)d.getComponent("website")).getLatest());
221 		return 0;
222 	}
223 
224 	int help()
225 	{
226 		throw new Exception("For help, run digger without any arguments.");
227 	}
228 
229 	version (Windows)
230 	int getAllMSIs()
231 	{
232 		d.getVSInstaller().getAllMSIs();
233 		return 0;
234 	}
235 }
236 
237 int digger()
238 {
239 	version (D_Coverage)
240 	{
241 		import core.runtime;
242 		dmd_coverSetMerge(true);
243 	}
244 
245 	static void usageFun(string usage)
246 	{
247 		import std.algorithm, std.array, std.stdio, std.string;
248 		auto lines = usage.splitLines();
249 
250 		stderr.writeln("Digger v" ~ diggerVersion ~ " - a D source code building and archaeology tool");
251 		stderr.writeln("Created by Vladimir Panteleev <vladimir@thecybershadow.net>");
252 		stderr.writeln("https://github.com/CyberShadow/Digger");
253 		stderr.writeln();
254 		stderr.writeln("Configuration file: ", opts.configFile.value.exists ? opts.configFile.value : "(not present)");
255 		stderr.writeln("Working directory: ", config.config.local.workDir);
256 		stderr.writeln();
257 
258 		if (lines[0].canFind("ACTION [ACTION-ARGUMENTS]"))
259 		{
260 			lines =
261 				[lines[0].replace(" ACTION ", " [OPTION]... ACTION ")] ~
262 				getUsageFormatString!(structFun!Opts).splitLines()[1..$] ~
263 				lines[1..$];
264 
265 			stderr.writefln("%-(%s\n%)", lines);
266 			stderr.writeln();
267 			stderr.writeln("For help on a specific action, run: digger ACTION --help");
268 			stderr.writeln("For more information, see README.md.");
269 			stderr.writeln();
270 		}
271 		else
272 			stderr.writefln("%-(%s\n%)", lines);
273 	}
274 
275 	return funoptDispatch!(Digger, FunOptConfig.init, usageFun)([thisExePath] ~ (opts.action ? [opts.action.value] ~ opts.actionArguments : []));
276 }
277 
278 mixin main!digger;