1 module custom;
2 
3 import std.algorithm;
4 import std.array;
5 import std.conv;
6 import std.exception;
7 import std.file;
8 import std.getopt;
9 import std.path;
10 import std.stdio;
11 import std..string;
12 
13 import ae.sys.d.manager;
14 import ae.utils.array;
15 import ae.utils.json;
16 import ae.utils.regex;
17 
18 import common;
19 import config;
20 import install;
21 import repo;
22 
23 alias indexOf = std..string.indexOf;
24 
25 // https://issues.dlang.org/show_bug.cgi?id=15777
26 alias strip = std..string.strip;
27 
28 alias subDir!"result" resultDir;
29 
30 /// We save a JSON file to the result directory with the build parameters.
31 struct BuildInfo
32 {
33 	string diggerVersion;
34 	string spec;
35 	DiggerManager.Config.Build config;
36 	DManager.SubmoduleState components;
37 }
38 
39 enum buildInfoFileName = "build-info.json";
40 
41 void prepareResult()
42 {
43 	log("Moving...");
44 	if (resultDir.exists)
45 		resultDir.rmdirRecurse();
46 	rename(d.buildDir, resultDir);
47 
48 	log("Build successful.\n\nTo start using it, run `digger install`, or add %s to your PATH.".format(
49 		resultDir.buildPath("bin").absolutePath()
50 	));
51 }
52 
53 /// Build the customized D version.
54 /// The result will be in resultDir.
55 void runBuild(string spec, DManager.SubmoduleState submoduleState, bool asNeeded)
56 {
57 	auto buildInfoPath = buildPath(resultDir, buildInfoFileName);
58 	auto buildInfo = BuildInfo(diggerVersion, spec, d.config.build, submoduleState);
59 	if (asNeeded && buildInfoPath.exists && buildInfoPath.readText.jsonParse!BuildInfo == buildInfo)
60 	{
61 		log("Reusing existing version in " ~ resultDir);
62 		return;
63 	}
64 	d.build(submoduleState);
65 	prepareResult();
66 	std.file.write(buildInfoPath, buildInfo.toJson());
67 }
68 
69 /// Perform an incremental build, i.e. don't clean or fetch anything from remote repos
70 void incrementalBuild()
71 {
72 	d.rebuild();
73 	prepareResult();
74 }
75 
76 /// Run tests.
77 void runTests()
78 {
79 	d.test();
80 }
81 
82 /// Implements transient persistence for the current customization state.
83 struct DCustomizer
84 {
85 	struct CustomizationState
86 	{
87 		string spec;
88 		DManager.SubmoduleState submoduleState;
89 		string[string][string] pulls;
90 		string[string][string][string] forks;
91 	}
92 	CustomizationState state;
93 
94 	enum fileName = "customization-state.json";
95 
96 	void load()
97 	{
98 		state = fileName.readText().jsonParse!CustomizationState();
99 	}
100 
101 	void save()
102 	{
103 		std.file.write(fileName, state.toJson());
104 	}
105 
106 	void finish()
107 	{
108 		std.file.remove(fileName);
109 	}
110 
111 	string getPull(string submoduleName, string pullNumber)
112 	{
113 		string rev = state.pulls.get(submoduleName, null).get(pullNumber, null);
114 		if (!rev)
115 			state.pulls[submoduleName][pullNumber] = rev = d.getPull(submoduleName, pullNumber.to!int);
116 		return rev;
117 	}
118 
119 	string getFork(string submoduleName, string user, string branch)
120 	{
121 		string rev = state.forks.get(submoduleName, null).get(user, null).get(branch, null);
122 		if (!rev)
123 			state.forks[submoduleName][user][branch] = rev = d.getFork(submoduleName, user, branch);
124 		return rev;
125 	}
126 }
127 
128 DCustomizer customizer;
129 
130 int handleWebTask(string[] args)
131 {
132 	enforce(args.length, "No task specified");
133 	switch (args[0])
134 	{
135 		case "initialize":
136 			d.needUpdate();
137 			log("Ready.");
138 			return 0;
139 		case "begin":
140 			d.haveUpdate = true; // already updated in "initialize"
141 			customizer.state.spec = "digger-web @ " ~ (args.length == 1 ? "(master)" : args[1]);
142 			customizer.state.submoduleState = d.begin(parseRev(args.length == 1 ? null : args[1]));
143 			customizer.save();
144 			log("Ready.");
145 			return 0;
146 		case "merge":
147 			enforce(args.length == 3);
148 			customizer.load();
149 			d.merge(customizer.state.submoduleState, args[1], customizer.getPull(args[1], args[2]));
150 			customizer.save();
151 			return 0;
152 		case "unmerge":
153 			enforce(args.length == 3);
154 			customizer.load();
155 			d.unmerge(customizer.state.submoduleState, args[1], customizer.getPull(args[1], args[2]));
156 			customizer.save();
157 			return 0;
158 		case "merge-fork":
159 			enforce(args.length == 4);
160 			customizer.load();
161 			d.merge(customizer.state.submoduleState, args[2], customizer.getFork(args[2], args[1], args[3]));
162 			customizer.save();
163 			return 0;
164 		case "unmerge-fork":
165 			enforce(args.length == 4);
166 			customizer.load();
167 			d.unmerge(customizer.state.submoduleState, args[2], customizer.getFork(args[2], args[1], args[3]));
168 			customizer.save();
169 			return 0;
170 		case "callback":
171 			d.callback(args[1..$]);
172 			return 0;
173 		case "build":
174 		{
175 			customizer.load();
176 
177 			string model;
178 			getopt(args,
179 				"model", &model,
180 			);
181 			enforce(args.length == 1, "Unrecognized build option");
182 
183 			if (model.length)
184 				d.config.build.components.common.models = [model];
185 
186 			runBuild(customizer.state.spec, customizer.state.submoduleState, false);
187 			customizer.finish();
188 			return 0;
189 		}
190 		case "branches":
191 			foreach (line; d.getMetaRepo().git.query("branch", "--remotes").splitLines())
192 				if (line.startsWith("  origin/") && line[2..$].indexOf(" ") < 0)
193 					writeln(line[9..$]);
194 			return 0;
195 		case "tags":
196 			d.getMetaRepo().git.run("tag");
197 			return 0;
198 		case "install-preview":
199 			install.install(false, true);
200 			return 0;
201 		case "install":
202 			install.install(true, false);
203 			return 0;
204 		default:
205 			assert(false);
206 	}
207 }
208 
209 DManager.SubmoduleState parseSpec(string spec)
210 {
211 	auto parts = spec.split("+");
212 	parts = parts.map!strip().array();
213 	if (parts.empty)
214 		parts = [null];
215 	auto rev = parseRev(parts.shift());
216 
217 	auto state = d.begin(rev);
218 
219 	foreach (part; parts)
220 	{
221 		bool revert = part.skipOver("-");
222 
223 		void handleCommit(string component, string commit, int mainline)
224 		{
225 			if (revert)
226 				d.revert(state, component, commit, mainline);
227 			else
228 				d.merge(state, component, commit);
229 		}
230 
231 		void handleBranch(string component, string branch)
232 		{
233 			if (revert)
234 			{
235 				string commit; int mainline;
236 				d.getChild(state, component, branch, /*out*/commit, /*out*/mainline);
237 				handleCommit(component, commit, mainline);
238 			}
239 			else
240 				handleCommit(component, branch, 0);
241 		}
242 
243 		if (part.matchCaptures(re!`^(\w[\w\-\.]*)#(\d+)$`,
244 			(string component, int pull)
245 			{
246 				handleBranch(component, d.getPull(component, pull));
247 			}))
248 			continue;
249 
250 		if (part.matchCaptures(re!`^([a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])/(\w[\w\-\.]*)/(\w[\w\-]*)$`,
251 			(string user, string component, string branch)
252 			{
253 				handleBranch(component, d.getFork(component, user, branch));
254 			}))
255 			continue;
256 
257 		if (part.matchCaptures(re!`^(\w+)/([0-9a-fA-F]{40})$`,
258 			(string component, string commit)
259 			{
260 				handleCommit(component, commit, 0);
261 			}))
262 			continue;
263 
264 		throw new Exception("Don't know how to apply customization: " ~ spec);
265 	}
266 
267 	return state;
268 }
269 
270 /// Build D according to the given spec string
271 /// (e.g. master+dmd#123).
272 void buildCustom(string spec, bool asNeeded = false)
273 {
274 	log("Building spec: " ~ spec);
275 	auto submoduleState = parseSpec(spec);
276 	runBuild(spec, submoduleState, asNeeded);
277 }
278 
279 void checkout(string spec)
280 {
281 	log("Checking out: " ~ spec);
282 	auto submoduleState = parseSpec(spec);
283 	d.checkout(submoduleState);
284 	log("Done.");
285 }
286 
287 /// Build all D versions (for the purpose of caching them).
288 /// Build order is in steps of decreasing powers of two.
289 void buildAll(string spec)
290 {
291 	d.needUpdate();
292 	auto commits = d.getLog("refs/remotes/origin/" ~ spec);
293 	commits.reverse(); // oldest first
294 
295 	for (int step = 1 << 30; step; step >>= 1)
296 	{
297 		if (step >= commits.length)
298 			continue;
299 
300 		log("Building all revisions with step %d (%d/%d revisions)".format(step, commits.length/step, commits.length));
301 
302 		for (int n = step; n < commits.length; n += step)
303 			try
304 			{
305 				auto state = d.begin(commits[n].hash);
306 				if (!d.isCached(state))
307 				{
308 					log("Building revision %d/%d".format(n/step, commits.length/step));
309 					d.build(state);
310 				}
311 			}
312 			catch (Exception e)
313 				log(e.toString());
314 	}
315 }