let's do this too
[QuestHelper.git] / Development / build
blob3cfb90cba4476cee8f2d4b208aed8866e2fa39be
1 #!/usr/bin/env lua
3 local compile=true
4 local external=true
5 local archive_zip=true
6 local archive_7z=false
7 local icons=true
8 local debug_build=false
9 local changes_lua = true
10 local condense = true
12 function SplitVersion(ver)
13   return string.gsub(ver, " on .*", ""), string.gsub(ver, ".* on ", "")
14 end
16 function AuthorizedVersion(ver)
17   qhv, wowv = SplitVersion(ver)
18   return qhv ~= "Development Version"
19 end
21 function PreWrath(ver)
22   qhv, wowv = SplitVersion(ver)
23   return wowv:sub(1,1) ~= '3'
24 end
26 for i, a in ipairs({...}) do
27   local mode, option = select(3, string.find(a, "^([%+%-]?)(.*)$"))
28   
29   mode = mode ~= "-"
30   
31   if option == "zip" then archive_zip = mode
32   elseif option == "7z" then archive_7z = mode
33   elseif option == "compile" then compile = mode
34   elseif option == "external" then external = mode
35   elseif option == "icons" then icons = mode
36   elseif option == "changes" then changes_lua = mode
37   elseif option == "debug" then debug_build = mode
38   elseif option == "condense" then condense = mode
39   else
40     print("Unknown option: "..option)
41     return 1
42   end
43 end
45 cache = {}
46 local cache_loader = loadfile("build-cache.lua")
47 if cache_loader then
48   cache_loader()
49   cache_loader = nil
50 end
52 if not cache.removed then cache.removed = {} end
53 if not cache.known then cache.known = {} end
54 if not cache.ignored then cache.ignored = {} end
55 if not cache.uid then cache.uid = {} end
57 local file2uid = {}
58 for uid, data in pairs(cache.uid) do file2uid[data.file] = uid end
60 -- so upgrade.lua doesn't freak out
61 GetTime = function () end
62 QuestHelper_File = {}
63 QuestHelper_Loadtime = {}
65 loadfile("dump.lua")()
66 loadfile("fileutil.lua")()
67 loadfile("../upgrade.lua")()
68 loadfile("compiler.lua")()
69 loadfile("pepperfish.lua")()
70 --loadfile("external.lua")()
72 profiler = newProfiler("time", 1000000)
73 --profiler:start()
75 QuestHelper_BuildZoneLookup()
77 if changes_lua then
78   local stream = io.open("../changes.txt", "r")
79   
80   if stream then
81     local function linkparse(a, b)
82       b = b:gsub("^%s*(.-)%s*$", "%1")
83       if b == "" then
84         return a
85       else
86         return b
87       end
88     end
89     
90     local function totext(text)
91       return text:gsub("%[%s*([^%s%]]+)(.-)%]", linkparse)
92                  :gsub("<code>(.-)</code>", "|cff40bbff%1|r")
93                  :gsub("^%s*==%s*(.-)%s*==%s*$", "|cffffff00       %1|r\n")
94                  :gsub("^%*%*%s", "    • ")
95                  :gsub("^%*%s", "• ").."\n"
96     end
97     
98     local count = 0
99     local text = ""
100     
101     while true do
102       line = stream:read("*l")
103       if not line then break end
104       if line:find("^%s*==.*==%s*$") then
105         count = count + 1
106         if count == 6 then
107           break
108         end
109         text = text .. (count == 1 and "" or "\n") .. totext(line)
110       elseif not line:find("^%s*$") then
111         text = text .. totext(line)
112       end
113     end
115     io.close(stream)
116     
117     stream = io.open("../changes.lua", "w")
118     if stream then
119       stream:write(("QuestHelper_ChangeLog=%q\n"):format(text))
120       io.close(stream) 
121     else
122       print("Error writing ../changes.lua")
123     end
124   else
125     print("Error opening ../changes.txt")
126   end
129 if external then
131   --print("Updating Astrolabe.")
132   --FileUtil.updateSVNRepo("http://svn.esamynn.org/astrolabe/branches/wotlk_data_temp", "../Astrolabe")
133   print("Updating ChatThrottleLib.")
134   FileUtil.updateSVNRepo("svn://svn.wowace.com/wow/chatthrottlelib/mainline/trunk", "../ChatThrottleLib")
137 if compile then
138   local function saveCache()
139     local stream = io.open("build-cache.lua", "w")
140     local buffer, prebuf = CreateBuffer(), CreateBuffer()
141     DumpVariable(buffer, prebuf, cache, "cache")
142     stream:write(DumpingComplete(buffer, prebuf))
143     io.close(stream)
144   end
145   
146   local function QuestHelper_IsCorrupted(data)
147     if type(data.QuestHelper_Objectives) == "table" then for version, package in pairs(data.QuestHelper_Objectives) do
148       if AuthorizedVersion(version) and type(package) == "table" then for category, objectives in pairs(package) do
149         if type(objectives) == "table" and PreWrath(version) then for name, objective in pairs(objectives) do
150           if type(objective) == "table" and objective.pos and type(objective.pos) == "table" then for i, pos in pairs(objective.pos) do
151             if pos[1] and pos[1] >= 65 then
152               print("SOMETHING IS SERIOUSLY WRONG, found Wrath coordinates in non-Wrath files (version " .. version .. ", continent " .. pos[1] .. ")")
153               return true
154             end
155           end end
156         end end
157       end end
158     end end
159   end
160   
161   -- Download the latest copy of the translated item/npc names.
162   if external then
163     os.execute("wget http://smariot.hopto.org/wowdata.7z -N")
164     
165     if FileUtil.fileExists("wowdata.7z") then
166       local hash = FileUtil.fileHash("wowdata.7z")
167       if cache.wowdata_hash ~= hash then
168         -- If the archive has changed, extract the lua file from the archive.
169         cache.wowdata_hash = hash
170         os.execute("7z e -y wowdata.7z wowdata.lua")
171       end
172     end
173   end
174   
175   if FileUtil.fileExists("wowdata.lua") then
176     loadfile("wowdata.lua")()
177   end
179   local all_input_files, unknown_input_files = {}, {}
181   local since_last_save = 0
183   FileUtil.forEachFile("LocalInput", function (name)
184     if FileUtil.extension(name) == "lua" or 
185        FileUtil.extension(name) == "bak" then
186        
187       local hash = FileUtil.fileHash(name)
188       if cache.ignored[hash] then return end
190       if since_last_save == 100 then
191         print("Collecting garbage and saving . . .")
192         collectgarbage("collect")
193         saveCache()
194         since_last_save = 0
195       end
196       since_last_save = since_last_save + 1
197     
198       cache.ignored[hash] = true
199       
200       local input = loadfile(name)
201       if input then
202         local data = {}
203         setfenv(input, data)
204         if pcall(input) then
205           if not (data.QuestHelper_Locale and data.QuestHelper_Objectives) then
206             print("! "..name.." isn't a QuestHelper SavedVariables file.")
207             return
208           end
209           
210           local upgradable_data = {}
211           setfenv(input, upgradable_data)
212           input()
213           QuestHelper_UpgradeDatabase(upgradable_data)
214           if QuestHelper_IsPolluted(upgradable_data) then
215             print("!! "..name.." is polluted")
216             return
217           end
218           if QuestHelper_IsCorrupted(upgradable_data) then
219             print("!! "..name.." is corrupted")
220             return
221           end
222           if not upgradable_data.QuestHelper_UID then
223             print("!! "..name.." has no UID")
224             return
225           end
226           
227           local tempname = os.tmpname()
228           local stream = io.open(tempname, "w")
229           if stream then
230             print("Copying/Sorting "..name)
231             stream:write(ScanAndDumpVariable(data, nil, true) or "")
232             io.close(stream)
233             hash = FileUtil.fileHash(tempname)
234             if hash then
235               local input_name = "Input/"..hash..".lua"
236               if not cache.removed[input_name] then
237                 if not cache.known[input_name] then
238                   FileUtil.copyFile(tempname, input_name)
239                   unknown_input_files[input_name] = name
240                 else
241                   cache.known[input_name] = name
242                 end
243               else
244                 cache.removed[input_name] = name
245               end
246             else
247               print("!!! Can't get hash of "..tempname..", for "..name)
248             end
249             FileUtil.unlinkFile(tempname)
250           end
251         else
252           print("!!! "..name.." couldn't be executed.")
253         end
254       else
255         print("!!! "..name.." couldn't be loaded.")
256       end
257     end
258   end)
260   collectgarbage("collect")
261   saveCache()
262   
263   FileUtil.forEachFile("Input", function (name)
264     if cache.removed[name] then
265       print("!!! Obsolete: ", cache.removed[name].." ("..name..")")
266       os.remove(name)
267     else
268       if not cache.known[name] then
269         unknown_input_files[name] = unknown_input_files[name] or name
270       end
271       
272       all_input_files[name] = cache.known[name] or unknown_input_files[name]
273     end
274   end)
276   local function ProcessObjective(category, name, objective, result)
277     local istring = "obj."..category.."."..name
278     
279     if category ~= "item" then
280       local seen = 0
281       if objective.pos then for i, pos in pairs(objective.pos) do
282         seen = seen + pos[4]
283       end end
284       
285       result[istring..".seen"] = (result[istring..".seen"] or 0) + seen
286     end
287     
288     if objective.vendor then
289       result[istring..".vend"] = (result[istring..".vend"] or 0) + #objective.vendor
290     end
291     
292     if objective.drop then for monster, count in pairs(objective.drop) do
293       result[istring] = (result[istring] or 0) + count
294     end end
295   end
297   local function ProcessQuest(faction, level, name, quest, result)
298     local qstring = "quest."..faction.."."..level.."."..name
299     result[qstring] = (result[qstring] or 0)+((quest.finish or quest.pos) and 1 or 0)
300     
301     if quest.item then for item_name, data in pairs(quest.item) do
302       ProcessObjective("item", item_name, data, result)
303     end end
304     
305     if quest.alt then for _, quest2 in pairs(quest.alt) do
306       ProcessQuest(faction, level, name, quest2, result)
307     end end
308   end
309   
310   local function LoadFile(file)
311     local data = loadfile(file)
312     local result = {}
313     if data then
314       local loaded = {}
315       if not pcall(setfenv(data, loaded)) then
316         print("!!!!! oh god something is wrong "..file)
317         return
318       end
319          
320       data()
321       
322       QuestHelper_UpgradeDatabase(loaded)
323       
324       if loaded.QuestHelper_UID then
325         result.uid = loaded.QuestHelper_UID
326         result.time = loaded.QuestHelper_SaveDate
327       else
328         if type(loaded.QuestHelper_Quests) == "table" then for version, package in pairs(loaded.QuestHelper_Quests) do
329           if AuthorizedVersion(version) and type(package) == "table" then for faction, levels in pairs(package) do
330             if type(levels) == "table" then for level, quest_list in pairs(levels) do
331               if type(quest_list) == "table" then for name, quest in pairs(quest_list) do
332                 ProcessQuest(faction, level, name, quest, result)
333               end end
334             end end
335           end end
336         end end
337         
338         if type(loaded.QuestHelper_Objectives) == "table" then for version, package in pairs(loaded.QuestHelper_Objectives) do
339           if AuthorizedVersion(version) and type(package) == "table" then for faction, levels in pairs(package) do
340             if type(objectives) == "table" then for name, objective in pairs(objectives) do
341               ProcessObjective(category, name, objective, result)
342             end end
343           end end
344         end end
345       end
346     end
347     
348     return result
349   end
351   local function ObsoletedBy(data1, data2)
352     if data1.uid or data2.uid then
353       return data1.loc == data2.loc and data1.uid == data2.uid and (data1.time or 0) >= (data2.time or 0)
354     end
355     
356     for key, value in pairs(data1) do
357       local value2 = data2[key]
358       if value2 == nil or value2 < value then
359         return false
360       end
361     end
362     return true
363   end
365   local checked = {}
366   local file_data = {}
367   
368   -- This entire mess is built to find obsolete files and remove them.
369   for new_name, original_name in pairs(unknown_input_files) do
370     print("Checking: ", original_name)
371     local data = file_data[new_name]
372     if not data then
373       data = LoadFile(new_name)
374       file_data[new_name] = data
375     end
376     cache.known[new_name] = original_name
377     checked[new_name] = true
378     
379     if data then
380       local uid, last_save = data.uid, data.time
381       
382       if uid then
383         local existing = cache.uid[uid]
384         if not existing then
385           cache.uid[uid] = {file=new_name, save=last_save}
386           file2uid[new_name] = uid
387         else
388           if existing.save >= last_save then
389             print("!!! Obsolete: ", original_name)
390             print("!!!       By: ", all_input_files[existing.file])
391             print("")
392             os.remove(new_name)
393             file_data[new_name] = nil
394             all_input_files[new_name] = nil
395             cache.removed[new_name] = original_name
396             cache.known[new_name] = nil
397             unknown_input_files[new_name] = nil
398           else
399             print("!!! Obsolete: ", all_input_files[existing.file])
400             print("!!!       By: ", original_name)
401             print("")
402             
403             os.remove(existing.file)
404             file_data[existing.file] = nil
405             file2uid[existing.file] = nil
406             file2uid[new_name] = uid
407             cache.removed[existing.file] = all_input_files[existing.file]
408             all_input_files[existing.file] = nil
409             cache.known[existing.file] = nil
410             existing.file = new_name
411           end
412         end
413       else
414         for existing_name, existing_original_name in pairs(all_input_files) do
415           if not checked[existing_name] and not file2uid[existing_name] then
416             local data2 = file_data[existing_name]
417             if not data2 then
418               data2 = LoadFile(existing_name)
419               file_data[existing_name] = data2
420             end
421             
422             if data2 and not data2.uid then
423               if ObsoletedBy(data, data2) then
424                 print("!!! Obsolete: ", original_name)
425                 print("!!!       By: ", existing_original_name)
426                 print("")
427                 
428                 os.remove(new_name)
429                 file_data[new_name] = nil
430                 all_input_files[new_name] = nil
431                 cache.removed[new_name] = original_name
432                 cache.known[new_name] = nil
433                 unknown_input_files[new_name] = nil
434                 break
435               elseif ObsoletedBy(data2, data) then
436                 print("!!! Obsolete: ", existing_original_name)
437                 print("!!!       By: ", original_name)
438                 print("")
439                 
440                 os.remove(existing_name)
441                 file_data[existing_name] = nil
442                 all_input_files[existing_name] = nil
443                 cache.removed[existing_name] = existing_original_name
444                 cache.known[existing_name] = nil
445                 unknown_input_files[existing_name] = nil
446               end
447             end
448           end
449         end
450       end
451     end
452   end
453   
454   checked, file_data = nil, nil
455   
456   saveCache()
457   collectgarbage("collect")
459   --print("Compiling Lightheaded/eql3 data. . .")
460   --ProcessExternal()
461   
462   local total_count = 0
463   local counter = 0
464   local pairfrequencies = {}
465   for name, origin in pairs(all_input_files) do
466     total_count = total_count + 1       -- I fuckin' hate Lua
467   end
468   for name, origin in pairs(all_input_files) do
469     counter = counter + 1
470     print("Compiling " .. counter .. "/" .. total_count .. ": ", origin)
471     --CompileInputFile(name)
472     local ret, msg = pcall(CompileInputFile, name, pairfrequencies)
473     if not ret then
474       print("!!!!! FAILURE")
475       print(msg)
476     end
477   end
478   
479   print("Processing final data")
480   local finaldata = CompileFinish()
481   
482   local finalstatic = io.open("../static.lua", "w")
483   finalstatic:write("QuestHelper_File[\"static.lua\"] = \"Development Version\"\n")
484   finalstatic:write("QuestHelper_Loadtime[\"static.lua\"] = GetTime()\n")
485   finalstatic:write("QuestHelper_StaticData={")
486   
487   for k, v in pairs(finaldata) do
488     collectgarbage("collect")
489     local foloc = string.format("static_%s.lua", k)
490     local finalout = "../" .. foloc
491     local tempout = finalout
492     if condense then tempout = string.format("static_%s.large", k) end
493     
494     print("Writing: " .. tempout)
495     local stream = io.open(tempout, "w")
496     stream:write(string.format("QuestHelper_File[\"%s\"] = \"Development Version\"\n", foloc))
497     stream:write(string.format("QuestHelper_Loadtime[\"%s\"] = GetTime()\n", foloc))
498     stream:write(string.format("if GetLocale() ~= \"%s\" then return end\n", k))  -- wellp
499     stream:write(ScanAndDumpVariable(v, string.format("QuestHelper_StaticData_%s", k)))
500     io.close(stream)
501     
502     if condense then
503       print("Condensing " .. finalout)
504       assert(os.execute(string.format("lua LuaSrcDiet.lua --maximum %s -o %s", tempout, finalout)) ~= -1)
505       if false then
506         local data_loader = loadfile("LuaSrcDiet.lua")
507         local data = {arg={"--maximum", tempout, "-o", finalout}, TEST=true, string=string, print=print, table=table, ipairs=ipairs, error=error, io=io, tonumber=tonumber --[[ assert=assert, FileUtil=FileUtil, tostring=tostring ]]}  -- there has to be a better way to do this
508         setfenv(data_loader, data)
509         data_loader()
510         data.main()
511       end
512     end
513     
514     finalstatic:write(string.format("  %s=QuestHelper_StaticData_%s,", k, k))
515     --assert(FileUtil.fileContains("../QuestHelper.toc", foloc), "didn't contain " .. foloc)
516   end
517   
518   finalstatic:write("}")
519   io.close(finalstatic)
520   
521   local qhwowv = {}
522   local qhfrequencies = {}
523   local wowfrequencies = {}
524   
525   for k, v in pairs(pairfrequencies) do
526     table.insert(qhwowv, {freq = v, tag = k})
527     local qh, wow = SplitVersion(k)
528     qhfrequencies[qh] = (qhfrequencies[qh] or 0) + v
529     wowfrequencies[wow] = (wowfrequencies[wow] or 0) + v
530   end
531   
532   local qhv = {}
533   local wowv = {}
534   
535   for k, v in pairs(qhfrequencies) do table.insert(qhv, {freq = v, tag = k}) end
536   for k, v in pairs(wowfrequencies) do table.insert(wowv, {freq = v, tag = k}) end
537   
538   table.sort(qhwowv, function (a, b) return a.freq < b.freq end)
539   table.sort(qhv, function (a, b) return a.freq < b.freq end)
540   table.sort(wowv, function (a, b) return a.freq < b.freq end)
542   for k, v in pairs(qhwowv) do
543     print(string.format("%d: %s", v.freq, v.tag))
544   end
545   print("")
546   for k, v in pairs(qhv) do
547     print(string.format("%d: %s", v.freq, v.tag))
548   end
549   print("")
550   for k, v in pairs(wowv) do
551     print(string.format("%d: %s", v.freq, v.tag))
552   end
553 elseif not FileUtil.fileExists("../static.lua") then
554   print("../static.lua doesn't exist; you can't skip the compile step.")
555   return 1
558 if icons then
559   print("Creating: Icons.tga")
560   --if not FileUtil.fileExists("../Art/Icons.tga") then
561   --  print("You'll need to manually create Art/Icons.tga, ImageMagick's SVG support seems to have been broken recently.")
562   --end
563   FileUtil.convertImage("Data/art.svg", "../Art/Icons.tga")
566 local version_string = "UNKNOWN"
568 if archive_zip or archive_7z then
569   FileUtil.unlinkDirectory("QuestHelper")
570   
571   FileUtil.createDirectory("QuestHelper")
572   FileUtil.createDirectory("QuestHelper/Art")
573   FileUtil.createDirectory("QuestHelper/Fonts")
574   
575   local info = nil
576   
577   -- Try to coax git to give us something to use for the version.
578   local stream = io.popen("git describe --tags HEAD", "r")
579   if stream then
580     info = stream:read("*line")
581     if info then
582       -- Replace the first dash with a dot.
583       info = string.gsub(info, "^(.*)%-(.*)%-g(.*)$", "%1%.%2%-%3", 1)
584     end
585     io.close(stream)
586   end
587   
588   if not info then
589     -- Didn't get a nice looking tag to use, try to at least get a hash for the version.
590     stream = io.popen("git-log -1 --pretty=\"format:%2-%h\"", "r")
591     if stream then
592       info = stream:read("*line")
593       io.close(stream)
594     end
595   end
596   
597   if not info then
598     -- We have no idea what version this is, will use the string already in the TOC.
599     info = "%2"
600   end
601   
602   FileUtil.copyFile("../QuestHelper.toc", "QuestHelper", "^(##%s-Version%s-):%s*(.*)%s*$",
603     function (a, b)
604       version_string = string.gsub(info, "%%2", b) return a..": "..version_string
605     end)
606   
607   local file_version_pattern = "%1"..string.format("%q", version_string)
608   
609   FileUtil.forEachFile("..", function (file)
610     local ext = FileUtil.extension(file)
611     if ext == "lua" or ext == "xml" then
612       if debug_build then
613         FileUtil.copyFile(file, "QuestHelper")
614       else
615         FileUtil.copyFile(file, "QuestHelper",
616                                                "(assert%s*%b())", "--[[ %1 ]]",
617                                                "(QuestHelper:Assert%s*%b())", "--[[ %1 ]]",
618                                                "(QuestHelper_File%[[^%]]*%]%s*=%s*)\"[^\"]+\"", file_version_pattern)
619       end
620     end
621   end)
622   
623   FileUtil.forEachFile("../Art", function (file)
624     local ext = FileUtil.extension(file)
625     if ext == "blp" or ext == "tga" then
626       FileUtil.copyFile(file, "QuestHelper/Art")
627     end
628   end)
629   
630   FileUtil.forEachFile("../Fonts", function (file)
631     local ext = FileUtil.extension(file)
632     if ext == "txt" or ext == "ttf" then
633       FileUtil.copyFile(file, "QuestHelper/Fonts")
634     end
635   end)
636   
637   for i, dir in ipairs({"AstrolabeQH", "ChatThrottleLib", "LibAboutPanel", "lang"}) do
638     FileUtil.copyDirectoryRecursively("../"..dir, "QuestHelper/"..dir)
639   end
640   
641   if archive_zip then
642     local archive = "../QuestHelper-"..version_string..".zip"
643     print("Creating "..archive)
644     FileUtil.unlinkFile(archive)
645     FileUtil.createZipArchive("QuestHelper", archive)
646   end
647   
648   if archive_7z then
649     local archive = "../QuestHelper-"..version_string..".7z"
650     print("Creating "..archive)
651     FileUtil.unlinkFile(archive)
652     FileUtil.create7zArchive("QuestHelper", archive)
653   end
654   
655   FileUtil.unlinkDirectory("QuestHelper")
658 print("Done!")
660 --profiler:stop()
661 --local outfile = io.open("profile.txt", "w+")
662 --profiler:report(outfile)
663 --outfile:close()