update_responses 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. #!/usr/bin/perl -w
  2. use strict;
  3. use feature "state";
  4. use English;
  5. use FindBin;
  6. use YAML::XS qw(LoadFile);
  7. use File::Slurp;
  8. use File::Path qw(make_path);
  9. use Digest::SHA qw(sha256_hex);
  10. use XML::Writer;
  11. use Cwd;
  12. use File::Copy;
  13. use File::Temp;
  14. use File::Find;
  15. use POSIX qw(setlocale LC_ALL);
  16. use IO::CaptureOutput qw(capture_exec);
  17. use Parallel::ForkManager;
  18. use File::Basename;
  19. use XML::LibXML '1.70';
  20. use LWP::Simple;
  21. use JSON;
  22. # Set umask and locale to provide a consistent environment for MAR file
  23. # generation, etc.
  24. umask(0022);
  25. $ENV{"LC_ALL"} = "C";
  26. setlocale(LC_ALL, "C");
  27. my $htdocsdir = "$FindBin::Bin/htdocs";
  28. my $config = LoadFile("$FindBin::Bin/config.yml");
  29. my %htdocsfiles;
  30. my $releases_dir = $config->{releases_dir};
  31. $releases_dir = "$FindBin::Bin/$releases_dir" unless $releases_dir =~ m/^\//;
  32. my @check_errors;
  33. my $initPATH = $ENV{PATH};
  34. my $initLD_LIBRARY_PATH = $ENV{LD_LIBRARY_PATH};
  35. sub exit_error {
  36. print STDERR "Error: ", $_[0], "\n";
  37. chdir '/';
  38. exit (exists $_[1] ? $_[1] : 1);
  39. }
  40. sub get_tmpdir {
  41. my ($config) = @_;
  42. return File::Temp->newdir($config->{tmp_dir} ?
  43. (DIR => $config->{tmp_dir})
  44. : ());
  45. }
  46. sub build_targets_by_os {
  47. exit_error "Unknown build target for OS $_[0]" unless $config->{build_targets}{$_[0]};
  48. my $r = $config->{build_targets}{$_[0]};
  49. return ref $r eq 'ARRAY' ? @$r : ($r);
  50. }
  51. sub get_nbprocs {
  52. return $ENV{NUM_PROCS} if defined $ENV{NUM_PROCS};
  53. if (-f '/proc/cpuinfo') {
  54. return scalar grep { m/^processor\s+:\s/ } read_file '/proc/cpuinfo';
  55. }
  56. return 4;
  57. }
  58. sub write_htdocs {
  59. my ($channel, $file, $content) = @_;
  60. mkdir $htdocsdir unless -d $htdocsdir;
  61. mkdir "$htdocsdir/$channel" unless -d "$htdocsdir/$channel";
  62. write_file("$htdocsdir/$channel/$file", $content);
  63. $htdocsfiles{$channel}->{$file} = 1;
  64. }
  65. sub clean_htdocs {
  66. my (@channels) = @_;
  67. foreach my $channel (@channels) {
  68. opendir(my $d, "$htdocsdir/$channel");
  69. my @files = grep { ! $htdocsfiles{$channel}->{$_} } readdir $d;
  70. closedir $d;
  71. unlink map { "$htdocsdir/$channel/$_" } @files;
  72. }
  73. }
  74. sub get_sha512_hex_of_file {
  75. my ($file) = @_;
  76. my $sha = Digest::SHA->new("512");
  77. $sha->addfile($file);
  78. return $sha->hexdigest;
  79. }
  80. sub get_version_files {
  81. my ($config, $version) = @_;
  82. return if $config->{versions}{$version}{files};
  83. my $appname = $config->{appname_marfile};
  84. my $files = {};
  85. my $vdir = version_dir($config, $version);
  86. my $download_url = "$config->{download}{mars_url}/$version";
  87. opendir(my $d, $vdir) or exit_error "Error opening directory $vdir";
  88. foreach my $file (readdir $d) {
  89. next unless -f "$vdir/$file";
  90. if ($file !~ m/incremental\.mar$/ &&
  91. $file =~ m/^$appname-(.+)-${version}_(.+)\.mar$/) {
  92. my ($os, $lang) = ($1, $2);
  93. $files->{$os}{$lang}{complete} = {
  94. type => 'complete',
  95. URL => "$download_url/$file",
  96. size => -s "$vdir/$file",
  97. hashFunction => 'SHA512',
  98. hashValue => get_sha512_hex_of_file("$vdir/$file"),
  99. };
  100. next;
  101. }
  102. if ($file =~ m/^$appname-(.+)--(.+)-${version}_(.+)\.incremental\.mar$/) {
  103. my ($os, $from_version, $lang) = ($1, $2, $3);
  104. $files->{$os}{$lang}{partial}{$from_version} = {
  105. type => 'partial',
  106. URL => "$download_url/$file",
  107. size => -s "$vdir/$file",
  108. hashFunction => 'SHA512',
  109. hashValue => get_sha512_hex_of_file("$vdir/$file"),
  110. }
  111. }
  112. }
  113. closedir $d;
  114. $config->{versions}{$version}{files} = $files;
  115. }
  116. sub get_version_downloads {
  117. my ($config, $version) = @_;
  118. my $downloads = {};
  119. my $vdir = version_dir($config, $version);
  120. my $download_url = "$config->{download}{bundles_url}/$version";
  121. opendir(my $d, $vdir) or exit_error "Error opening directory $vdir";
  122. foreach my $file (readdir $d) {
  123. next unless -f "$vdir/$file";
  124. my $os;
  125. if ($file =~ m/^$config->{appname_bundle}-macos-$version.dmg$/) {
  126. $os = 'macos';
  127. } elsif ($file =~ m/^$config->{appname_bundle}-(linux-i686|linux-x86_64)-${version}.tar.xz$/) {
  128. $os = $1;
  129. } elsif ($file =~ m/^$config->{appname_bundle}-windows-x86_64-portable-${version}.exe$/) {
  130. $os = 'win64';
  131. } elsif ($file =~ m/^$config->{appname_bundle}-windows-i686-portable-${version}.exe$/) {
  132. $os = 'win32';
  133. } else {
  134. next;
  135. }
  136. $downloads->{$os}{ALL} = {
  137. binary => "$download_url/$file",
  138. sig => "$download_url/$file.asc",
  139. };
  140. }
  141. closedir $d;
  142. $config->{versions}{$version}{downloads} = $downloads;
  143. }
  144. sub get_perplatform_downloads {
  145. my ($config, $version, $gittag) = @_;
  146. my $downloads = {};
  147. my $vdir = version_dir($config, $version);
  148. my $download_url = "$config->{download}{bundles_url}/$version";
  149. opendir(my $d, $vdir) or exit_error "Error opening directory $vdir";
  150. foreach my $file (readdir $d) {
  151. next unless -f "$vdir/$file";
  152. my $os;
  153. if ($file =~ m/^$config->{appname_bundle}-macos-$version.dmg$/) {
  154. $os = 'macos';
  155. } elsif ($file =~ m/^$config->{appname_bundle}-(linux-i686|linux-86_64)-${version}.tar.xz$/) {
  156. $os = $1;
  157. } elsif ($file =~ m/^$config->{appname_bundle}-(windows-i686|windows-86_64)-portable-${version}.exe$/) {
  158. $os = $1;
  159. } elsif ($file =~ m/^$config->{appname_bundle}-(android-armv7|android-x86|android-x86_64|android-aarch64)-${version}.apk$/) {
  160. $os = $1;
  161. } else {
  162. next;
  163. }
  164. $downloads->{$os} = {
  165. version => "$version",
  166. git_tag => "$gittag",
  167. binary => "$download_url/$file",
  168. sig => "$download_url/$file.asc",
  169. };
  170. }
  171. closedir $d;
  172. $config->{versions}{$version}{pp_downloads} = $downloads;
  173. }
  174. sub extract_mar {
  175. my ($mar_file, $dest_dir, $compression) = @_;
  176. my $old_cwd = getcwd;
  177. mkdir $dest_dir;
  178. chdir $dest_dir or exit_error "Cannot enter $dest_dir";
  179. my $res = system('mar', '-x', $mar_file);
  180. exit_error "Error extracting $mar_file" if $res;
  181. if ($compression ne 'bzip2' && $compression ne 'xz') {
  182. exit_error "Unknown compression format $compression";
  183. }
  184. my $compr_ext = $compression eq 'bzip2' ? 'bz2' : 'xz';
  185. my $compr_cmd = $compression eq 'bzip2' ? 'bunzip2' : 'unxz';
  186. my $uncompress_file = sub {
  187. return unless -f $File::Find::name;
  188. rename $File::Find::name, "$File::Find::name.$compr_ext";
  189. system($compr_cmd, "$File::Find::name.$compr_ext") == 0
  190. || exit_error "Error decompressing $File::Find::name";
  191. };
  192. find($uncompress_file, $dest_dir);
  193. my $manifest = -f 'updatev3.manifest' ? 'updatev3.manifest'
  194. : 'updatev2.manifest';
  195. my @lines = read_file($manifest) if -f $manifest;
  196. foreach my $line (@lines) {
  197. if ($line =~ m/^addsymlink "(.+)" "(.+)"$/) {
  198. exit_error "$mar_file: Could not create symlink $1 -> $2"
  199. unless symlink $2, $1;
  200. }
  201. }
  202. chdir $old_cwd;
  203. }
  204. sub mar_filename {
  205. my ($config, $appname, $version, $os, $lang) = @_;
  206. version_dir($config, $version) . "/$appname-$os-${version}_$lang.mar";
  207. }
  208. sub create_incremental_mar {
  209. my ($config, $pm, $from_version, $new_version, $os, $lang, $channel) = @_;
  210. my $appname = $config->{appname_marfile};
  211. my $mar_file = "$appname-$os--${from_version}-${new_version}_$lang.incremental.mar";
  212. my $mar_file_path = version_dir($config, $new_version) . '/' . $mar_file;
  213. if ($ENV{MAR_SKIP_EXISTING} && -f $mar_file_path) {
  214. print "Skipping $mar_file\n";
  215. return;
  216. }
  217. print "Starting $mar_file\n";
  218. my $download_url = "$config->{download}{mars_url}/$new_version";
  219. my $finished_file = sub {
  220. exit_error "Error creating $mar_file" unless $_[1] == 0;
  221. print "Finished $mar_file\n";
  222. $config->{versions}{$new_version}{files}{$os}{$lang}{partial}{$from_version} = {
  223. type => 'partial',
  224. URL => "$download_url/$mar_file",
  225. size => -s $mar_file_path,
  226. hashFunction => 'SHA512',
  227. hashValue => get_sha512_hex_of_file($mar_file_path),
  228. };
  229. };
  230. return if $pm->start($finished_file);
  231. my $tmpdir = get_tmpdir($config);
  232. my $mar_c_from = get_config($config, $from_version, $os, 'mar_compression');
  233. my $mar_c_new = get_config($config, $new_version, $os, 'mar_compression');
  234. extract_mar(mar_filename($config, $appname, $from_version, $os, $lang),
  235. "$tmpdir/A", $mar_c_from);
  236. extract_mar(mar_filename($config, $appname, $new_version, $os, $lang),
  237. "$tmpdir/B", $mar_c_new);
  238. # bug 26054: make sure previous macOS version is code signed
  239. if (!$ENV{NO_CODESIGNATURE} && ($os eq 'macos')
  240. && ! -f "$tmpdir/A/Contents/_CodeSignature/CodeResources") {
  241. exit_error "Missing code signature in $from_version while creating $mar_file";
  242. }
  243. if ($ENV{CHECK_CODESIGNATURE_EXISTS}) {
  244. unless (-f "$tmpdir/A/Contents/_CodeSignature/CodeResources"
  245. && -f "$tmpdir/B/Contents/_CodeSignature/CodeResources") {
  246. exit_error "Missing code signature while creating $mar_file";
  247. }
  248. }
  249. local $ENV{MOZ_PRODUCT_VERSION} = $new_version;
  250. local $ENV{MAR_CHANNEL_ID} = get_config($config, $new_version, $os, 'mar_channel_id');
  251. local $ENV{TMPDIR} = $tmpdir;
  252. my ($out, $err, $success) = capture_exec('make_incremental_update.sh',
  253. $mar_file_path, "$tmpdir/A", "$tmpdir/B");
  254. if (!$success) {
  255. unlink $mar_file_path if -f $mar_file_path;
  256. exit_error "making incremental mar:\n" . $err;
  257. }
  258. $pm->finish;
  259. }
  260. sub create_incremental_mars_for_version {
  261. my ($config, $version, $channel) = @_;
  262. my $pm = Parallel::ForkManager->new(get_nbprocs);
  263. $pm->run_on_finish(sub { $_[2]->(@_) });
  264. my $v = $config->{versions}{$version};
  265. foreach my $from_version (@{$v->{incremental_from}}) {
  266. $config->{versions}{$from_version} //= {};
  267. get_version_files($config, $from_version);
  268. my $from_v = $config->{versions}{$from_version};
  269. foreach my $os (keys %{$v->{files}}) {
  270. foreach my $lang (keys %{$v->{files}{$os}}) {
  271. next unless defined $from_v->{files}{$os}{$lang}{complete};
  272. create_incremental_mar($config, $pm, $from_version, $version, $os, $lang, $channel);
  273. }
  274. }
  275. }
  276. $pm->wait_all_children;
  277. }
  278. sub get_config {
  279. my ($config, $version, $os, $name) = @_;
  280. return $config->{versions}{$version}{$os}{$name}
  281. // $config->{versions}{$version}{$name}
  282. // $config->{$name};
  283. }
  284. sub version_dir {
  285. my ($config, $version) = @_;
  286. return get_config($config, $version, 'any', 'releases_dir') . "/$version";
  287. }
  288. sub channel_to_version {
  289. my ($config, @channels) = @_;
  290. return values %{$config->{channels}} unless @channels;
  291. foreach my $channel (@channels) {
  292. exit_error "Unknown channel $channel"
  293. unless $config->{channels}{$channel};
  294. }
  295. return map { $config->{channels}{$_} } @channels;
  296. }
  297. sub get_buildinfos {
  298. my ($config, $version) = @_;
  299. return if exists $config->{versions}{$version}{buildID};
  300. extract_martools($config, $version);
  301. my $files = $config->{versions}{$version}{files};
  302. foreach my $os (keys %$files) {
  303. foreach my $lang (keys %{$files->{$os}}) {
  304. next unless $files->{$os}{$lang}{complete};
  305. my $tmpdir = get_tmpdir($config);
  306. my $mar_compression = get_config($config, $version, $os, 'mar_compression');
  307. extract_mar(
  308. mar_filename($config, $config->{appname_marfile}, $version, $os, $lang),
  309. "$tmpdir",
  310. $mar_compression);
  311. my $appfile = "$tmpdir/application.ini" if -f "$tmpdir/application.ini";
  312. $appfile = "$tmpdir/Contents/Resources/application.ini"
  313. if -f "$tmpdir/Contents/Resources/application.ini";
  314. exit_error "Could not find application.ini" unless $appfile;
  315. foreach my $line (read_file($appfile)) {
  316. if ($line =~ m/^BuildID=(.*)$/) {
  317. $config->{versions}{$version}{buildID} = $1;
  318. return;
  319. }
  320. }
  321. exit_error "Could not extract buildID from application.ini";
  322. }
  323. }
  324. }
  325. sub get_response {
  326. my ($config, $version, $os, @patches) = @_;
  327. my $res;
  328. my $writer = XML::Writer->new(OUTPUT => \$res, ENCODING => 'UTF-8');
  329. $writer->xmlDecl;
  330. $writer->startTag('updates');
  331. if (get_config($config, $version, $os, 'unsupported')) {
  332. $writer->startTag('update',
  333. unsupported => 'true',
  334. detailsURL => get_config($config, $version, $os, 'detailsURL'),
  335. );
  336. goto CLOSETAGS;
  337. }
  338. my $minversion = get_config($config, $version, $os, 'minSupportedOSVersion');
  339. my $mininstruc = get_config($config, $version, $os, 'minSupportedInstructionSet');
  340. $writer->startTag('update',
  341. type => 'minor',
  342. displayVersion => $version,
  343. appVersion => $version,
  344. platformVersion => get_config($config, $version, $os, 'platformVersion'),
  345. buildID => get_config($config, $version, $os, 'buildID'),
  346. detailsURL => get_config($config, $version, $os, 'detailsURL'),
  347. actions => 'showURL',
  348. openURL => get_config($config, $version, $os, 'detailsURL'),
  349. defined $minversion ? ( minSupportedOSVersion => $minversion ) : (),
  350. defined $mininstruc ? ( minSupportedInstructionSet => $mininstruc ) : (),
  351. );
  352. foreach my $patch (@patches) {
  353. my @sorted_patch = map { $_ => $patch->{$_} } sort keys %$patch;
  354. $writer->startTag('patch', @sorted_patch);
  355. $writer->endTag('patch');
  356. }
  357. CLOSETAGS:
  358. $writer->endTag('update');
  359. $writer->endTag('updates');
  360. $writer->end;
  361. return $res;
  362. }
  363. sub write_responses {
  364. my ($config, @channels) = @_;
  365. @channels = keys %{$config->{channels}} unless @channels;
  366. foreach my $channel (@channels) {
  367. my $version = $config->{channels}{$channel};
  368. get_version_files($config, $version);
  369. get_buildinfos($config, $version);
  370. my $files = $config->{versions}{$version}{files};
  371. my $migrate_archs = $config->{versions}{$version}{migrate_archs} // {};
  372. foreach my $old_os (keys %$migrate_archs) {
  373. my $new_os = $migrate_archs->{$old_os};
  374. foreach my $lang (keys %{$files->{$new_os}}) {
  375. $files->{$old_os}{$lang}{complete} =
  376. $files->{$new_os}{$lang}{complete};
  377. }
  378. }
  379. foreach my $os (keys %$files) {
  380. foreach my $lang (keys %{$files->{$os}}) {
  381. my $resp = get_response($config, $version, $os,
  382. $files->{$os}{$lang}{complete});
  383. write_htdocs($channel, "$version-$os-$lang.xml", $resp);
  384. foreach my $from_version (keys %{$files->{$os}{$lang}{partial}}) {
  385. $resp = get_response($config, $version, $os,
  386. $files->{$os}{$lang}{complete},
  387. $files->{$os}{$lang}{partial}{$from_version});
  388. write_htdocs($channel, "$from_version-$version-$os-$lang.xml", $resp);
  389. }
  390. }
  391. }
  392. write_htdocs($channel, 'no-update.xml',
  393. '<?xml version="1.0" encoding="UTF-8"?>'
  394. . "\n<updates></updates>\n");
  395. }
  396. }
  397. sub write_htaccess {
  398. my ($config, @channels) = @_;
  399. @channels = keys %{$config->{channels}} unless @channels;
  400. my $flags = "[last]";
  401. foreach my $channel (@channels) {
  402. my $htaccess = "RewriteEngine On\n";
  403. $htaccess .= $config->{htaccess_rewrite_rules}{$channel} // '';
  404. my $version = $config->{channels}{$channel};
  405. my $migrate_langs = $config->{versions}{$version}{migrate_langs} // {};
  406. my $files = $config->{versions}{$version}{files};
  407. $htaccess .= "RewriteRule ^[^\/]+/$version/ no-update.xml $flags\n";
  408. foreach my $os (sort keys %$files) {
  409. foreach my $bt (build_targets_by_os($os)) {
  410. foreach my $lang (sort keys %{$files->{$os}}) {
  411. foreach my $from_version (sort keys %{$files->{$os}{$lang}{partial}}) {
  412. $htaccess .= "RewriteRule ^$bt/$from_version/$lang "
  413. . "$from_version-$version-$os-$lang.xml $flags\n";
  414. }
  415. $htaccess .= "RewriteRule ^$bt/[^\/]+/$lang "
  416. . "$version-$os-$lang.xml $flags\n";
  417. }
  418. foreach my $lang (sort keys %$migrate_langs) {
  419. $htaccess .= "RewriteRule ^$bt/[^\/]+/$lang "
  420. . "$version-$os-$migrate_langs->{$lang}.xml $flags\n";
  421. }
  422. $htaccess .= "RewriteRule ^$bt/ $version-$os-ALL.xml $flags\n";
  423. }
  424. }
  425. write_htdocs($channel, '.htaccess', $htaccess);
  426. }
  427. }
  428. sub write_downloads_json {
  429. my ($config, @channels) = @_;
  430. return unless $config->{create_downloads_json};
  431. @channels = keys %{$config->{channels}} unless @channels;
  432. foreach my $channel (@channels) {
  433. my $version = $config->{channels}{$channel};
  434. my $tag = get_config($config, $version, 'any', 'tag');
  435. my $data = {
  436. version => "$version",
  437. tag => "$tag",
  438. downloads => get_version_downloads($config, $version),
  439. };
  440. write_htdocs($channel, 'downloads.json',
  441. JSON->new->utf8->canonical->encode($data));
  442. my $pp_downloads = get_perplatform_downloads($config, $version, $tag);
  443. foreach my $os (keys %{$pp_downloads}) {
  444. write_htdocs($channel, "download-$os.json",
  445. JSON->new->utf8->canonical->encode($pp_downloads->{$os}));
  446. }
  447. }
  448. }
  449. sub marzip_path {
  450. my ($config, $version) = @_;
  451. for my $osname (qw/linux-x86_64 linux-i686 macos-x86_64 windows-x86_64 windows-i686/) {
  452. my $marzip = glob(version_dir($config, $version) . "/mar-tools-$osname-*.zip");
  453. if ($marzip && -f $marzip) {
  454. return $marzip;
  455. }
  456. }
  457. exit_error 'Could not find mar-tools';
  458. }
  459. my $martools_tmpdir;
  460. sub extract_martools {
  461. my ($config, $version) = @_;
  462. my $marzip = marzip_path($config, $version);
  463. $martools_tmpdir = get_tmpdir($config);
  464. my $old_cwd = getcwd;
  465. chdir $martools_tmpdir;
  466. my (undef, undef, $success) = capture_exec('unzip', $marzip);
  467. chdir $old_cwd;
  468. exit_error "Error extracting $marzip" unless $success;
  469. $ENV{PATH} = "$martools_tmpdir/mar-tools:$initPATH";
  470. if ($initLD_LIBRARY_PATH) {
  471. $ENV{LD_LIBRARY_PATH} = "$initLD_LIBRARY_PATH:$martools_tmpdir/mar-tools";
  472. } else {
  473. $ENV{LD_LIBRARY_PATH} = "$martools_tmpdir/mar-tools";
  474. }
  475. }
  476. sub log_step {
  477. my ($url, $step, $status, $details) = @_;
  478. state $u;
  479. if (!defined $u || $url ne $u) {
  480. print "\n" if $u;
  481. print "$url\n";
  482. $u = $url;
  483. }
  484. print ' ', $step, $status ? ': OK' : ': ERROR',
  485. $details ? " - $details\n" : "\n";
  486. return if $status;
  487. push @check_errors, { url => $url, step => $step, details => $details };
  488. }
  489. sub get_remote_xml {
  490. my ($url) = @_;
  491. my $content = get $url;
  492. log_step($url, 'get', defined $content);
  493. return undef unless defined $content;
  494. my $dom = eval { XML::LibXML->load_xml(string => $content) };
  495. log_step($url, 'parse_xml', defined $dom, $@);
  496. return $dom;
  497. }
  498. sub check_get_version {
  499. my ($dom) = @_;
  500. my @updates = $dom->documentElement()->getChildrenByLocalName('update');
  501. return undef unless @updates;
  502. return $updates[0]->getAttribute('appVersion');
  503. }
  504. sub check_no_update {
  505. my ($dom) = @_;
  506. my @updates = $dom->documentElement()->getChildrenByLocalName('update');
  507. return @updates == 0;
  508. }
  509. sub check_has_incremental {
  510. my ($dom) = @_;
  511. my @updates = $dom->documentElement()->getChildrenByLocalName('update');
  512. return undef unless @updates;
  513. my @patches = $updates[0]->getChildrenByLocalName('patch');
  514. foreach my $patch (@patches) {
  515. return 1 if $patch->getAttribute('type') eq 'partial';
  516. }
  517. return undef;
  518. }
  519. sub build_targets_list {
  520. map { ref $_ eq 'ARRAY' ? @$_ : $_ } values %{$config->{build_targets}};
  521. }
  522. sub check_update_responses_channel {
  523. my ($config, $base_url, $channel) = @_;
  524. my $channel_version = $config->{channels}{$channel};
  525. foreach my $build_target (build_targets_list()) {
  526. foreach my $lang (qw(en-US de)) {
  527. my $url = "$base_url/$channel/$build_target/1.0/$lang";
  528. my $dom = get_remote_xml($url);
  529. if ($dom) {
  530. my $version = check_get_version($dom);
  531. log_step($url, 'version', $version eq $channel_version,
  532. "expected: $channel_version received: $version");
  533. }
  534. $url = "$base_url/$channel/$build_target/$channel_version/$lang";
  535. $dom = get_remote_xml($url);
  536. log_step($url, 'no_update', check_no_update($dom)) if $dom;
  537. my @inc = @{$config->{versions}{$channel_version}{incremental_from}}
  538. if $config->{versions}{$channel_version}{incremental_from};
  539. foreach my $inc_from (@inc) {
  540. my $url = "$base_url/$channel/$build_target/$inc_from/$lang";
  541. $dom = get_remote_xml($url);
  542. next unless $dom;
  543. my $version = check_get_version($dom);
  544. log_step($url, 'version', $version eq $channel_version,
  545. "expected: $channel_version received: $version");
  546. log_step($url, 'has_incremental', check_has_incremental($dom));
  547. }
  548. }
  549. }
  550. }
  551. sub download_version {
  552. my ($config, $version) = @_;
  553. my $tmpdir = get_tmpdir($config);
  554. my $destdir = version_dir($config, $version);
  555. my $urldir = "$config->{download}{archive_url}/$version";
  556. print "Downloading version $version\n";
  557. foreach my $file (qw(sha256sums-signed-build.txt sha256sums-signed-build.txt.asc)) {
  558. if (getstore("$urldir/$file", "$tmpdir/$file") != 200) {
  559. exit_error "Error downloading $urldir/$file";
  560. }
  561. }
  562. if (system('gpg', '--no-default-keyring', '--keyring',
  563. "$FindBin::Bin/$config->{download}{gpg_keyring}", '--verify',
  564. "$tmpdir/sha256sums-signed-build.txt.asc",
  565. "$tmpdir/sha256sums-signed-build.txt")) {
  566. exit_error "Error checking gpg signature for version $version";
  567. }
  568. make_path $destdir;
  569. move "$tmpdir/sha256sums-signed-build.txt.asc", "$destdir/sha256sums-signed-build.txt.asc";
  570. move "$tmpdir/sha256sums-signed-build.txt", "$destdir/sha256sums-signed-build.txt";
  571. my %sums = map { chomp; reverse split ' ', $_ }
  572. read_file "$destdir/sha256sums-signed-build.txt";
  573. foreach my $file (sort grep { $_ =~ m/\.mar$/ } keys %sums) {
  574. print "Downloading $file\n";
  575. exit_error "Error downloading $urldir/$file\n"
  576. unless getstore("$urldir/$file", "$tmpdir/$file") == 200;
  577. exit_error "Wrong checksum for $file"
  578. unless $sums{$file} eq sha256_hex(read_file("$tmpdir/$file"));
  579. move "$tmpdir/$file", "$destdir/$file";
  580. }
  581. }
  582. sub download_missing_versions {
  583. my ($config, @channels) = @_;
  584. foreach my $channel (@channels) {
  585. exit_error "Unknown channel $channel"
  586. unless $config->{channels}{$channel};
  587. my $cversion = $config->{channels}{$channel};
  588. next unless $config->{versions}{$cversion}{incremental_from};
  589. foreach my $version (@{$config->{versions}{$cversion}{incremental_from}}) {
  590. next if -d version_dir($config, $version);
  591. download_version($config, $version);
  592. }
  593. }
  594. }
  595. sub check_update_responses {
  596. my ($config) = @_;
  597. exit_error "usage: $PROGRAM_NAME <base_url> [channels...]" unless @ARGV;
  598. my ($base_url, @channels) = @ARGV;
  599. foreach my $channel (@channels ? @channels : keys %{$config->{channels}}) {
  600. check_update_responses_channel($config, $base_url, $channel);
  601. }
  602. if (!@check_errors) {
  603. print "\n\nNo errors\n";
  604. return;
  605. }
  606. print "\n\nErrors list:\n";
  607. my $url = '';
  608. foreach my $error (@check_errors) {
  609. if ($url ne $error->{url}) {
  610. $url = $error->{url};
  611. print "$url\n";
  612. }
  613. print " $error->{step}",
  614. $error->{details} ? " - $error->{details}\n" : "\n";
  615. }
  616. }
  617. my %actions = (
  618. update_responses => sub {
  619. my ($config) = @_;
  620. my @channels = @ARGV ? @ARGV : keys %{$config->{channels}};
  621. foreach my $channel (@channels) {
  622. exit_error "Unknown channel $channel"
  623. unless $config->{channels}{$channel};
  624. $htdocsfiles{$channel} = { '.' => 1, '..' => 1 };
  625. }
  626. write_responses($config, @channels);
  627. write_htaccess($config, @channels);
  628. write_downloads_json($config, @channels);
  629. clean_htdocs(@channels);
  630. },
  631. gen_incrementals => sub {
  632. my ($config) = @_;
  633. foreach my $channel (@ARGV) {
  634. my ($version) = channel_to_version($config, $channel);
  635. extract_martools($config, $version);
  636. get_version_files($config, $version);
  637. create_incremental_mars_for_version($config, $version, $channel);
  638. }
  639. },
  640. download_missing_versions => sub {
  641. my ($config) = @_;
  642. my @channels = @ARGV ? @ARGV : keys %{$config->{channels}};
  643. download_missing_versions($config, @channels);
  644. },
  645. check_update_responses_deployement => \&check_update_responses,
  646. get_channel_version => sub {
  647. my ($config) = @_;
  648. exit_error "Wrong arguments" unless @ARGV == 1;
  649. exit_error "Unknown channel" unless $config->{channels}{$ARGV[0]};
  650. print $config->{channels}{$ARGV[0]}, "\n";
  651. },
  652. );
  653. my $action = fileparse($PROGRAM_NAME);
  654. exit_error "Unknown action $action" unless $actions{$action};
  655. $actions{$action}->($config);