You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

Jar.java 24 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. /*
  2. * The Apache Software License, Version 1.1
  3. *
  4. * Copyright (c) 2000-2003 The Apache Software Foundation. All rights
  5. * reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions
  9. * are met:
  10. *
  11. * 1. Redistributions of source code must retain the above copyright
  12. * notice, this list of conditions and the following disclaimer.
  13. *
  14. * 2. Redistributions in binary form must reproduce the above copyright
  15. * notice, this list of conditions and the following disclaimer in
  16. * the documentation and/or other materials provided with the
  17. * distribution.
  18. *
  19. * 3. The end-user documentation included with the redistribution, if
  20. * any, must include the following acknowlegement:
  21. * "This product includes software developed by the
  22. * Apache Software Foundation (http://www.apache.org/)."
  23. * Alternately, this acknowlegement may appear in the software itself,
  24. * if and wherever such third-party acknowlegements normally appear.
  25. *
  26. * 4. The names "The Jakarta Project", "Ant", and "Apache Software
  27. * Foundation" must not be used to endorse or promote products derived
  28. * from this software without prior written permission. For written
  29. * permission, please contact apache@apache.org.
  30. *
  31. * 5. Products derived from this software may not be called "Apache"
  32. * nor may "Apache" appear in their names without prior written
  33. * permission of the Apache Group.
  34. *
  35. * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
  36. * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  37. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  38. * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
  39. * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  40. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  41. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
  42. * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  43. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  44. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  45. * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  46. * SUCH DAMAGE.
  47. * ====================================================================
  48. *
  49. * This software consists of voluntary contributions made by many
  50. * individuals on behalf of the Apache Software Foundation. For more
  51. * information on the Apache Software Foundation, please see
  52. * <http://www.apache.org/>.
  53. */
  54. package org.apache.tools.ant.taskdefs;
  55. import java.io.ByteArrayInputStream;
  56. import java.io.ByteArrayOutputStream;
  57. import java.io.File;
  58. import java.io.FileReader;
  59. import java.io.IOException;
  60. import java.io.InputStream;
  61. import java.io.InputStreamReader;
  62. import java.io.OutputStreamWriter;
  63. import java.io.PrintWriter;
  64. import java.io.Reader;
  65. import java.util.Enumeration;
  66. import java.util.zip.ZipEntry;
  67. import java.util.zip.ZipFile;
  68. import org.apache.tools.ant.BuildException;
  69. import org.apache.tools.ant.Project;
  70. import org.apache.tools.ant.ResourceScanner;
  71. import org.apache.tools.ant.types.EnumeratedAttribute;
  72. import org.apache.tools.ant.types.FileSet;
  73. import org.apache.tools.ant.types.Resource;
  74. import org.apache.tools.ant.types.ZipFileSet;
  75. import org.apache.tools.zip.ZipOutputStream;
  76. /**
  77. * Creates a JAR archive.
  78. *
  79. * @author James Davidson <a href="mailto:duncan@x180.com">duncan@x180.com</a>
  80. * @author Brian Deitte
  81. * <a href="mailto:bdeitte@macromedia.com">bdeitte@macromedia.com</a>
  82. *
  83. * @since Ant 1.1
  84. *
  85. * @ant.task category="packaging"
  86. */
  87. public class Jar extends Zip {
  88. /** The index file name. */
  89. private static final String INDEX_NAME = "META-INF/INDEX.LIST";
  90. /** merged manifests added through addConfiguredManifest */
  91. private Manifest configuredManifest;
  92. /** shadow of the above if upToDate check alters the value */
  93. private Manifest savedConfiguredManifest;
  94. /** merged manifests added through filesets */
  95. private Manifest filesetManifest;
  96. /**
  97. * Manifest of original archive, will be set to null if not in
  98. * update mode.
  99. */
  100. private Manifest originalManifest;
  101. /**
  102. * whether to merge fileset manifests;
  103. * value is true if filesetmanifest is 'merge' or 'mergewithoutmain'
  104. */
  105. private FilesetManifestConfig filesetManifestConfig;
  106. /**
  107. * whether to merge the main section of fileset manifests;
  108. * value is true if filesetmanifest is 'merge'
  109. */
  110. private boolean mergeManifestsMain = true;
  111. /** the manifest specified by the 'manifest' attribute **/
  112. private Manifest manifest;
  113. /**
  114. * The file found from the 'manifest' attribute. This can be
  115. * either the location of a manifest, or the name of a jar added
  116. * through a fileset. If its the name of an added jar, the
  117. * manifest is looked for in META-INF/MANIFEST.MF
  118. */
  119. private File manifestFile;
  120. /** jar index is JDK 1.3+ only */
  121. private boolean index = false;
  122. /** constructor */
  123. public Jar() {
  124. super();
  125. archiveType = "jar";
  126. emptyBehavior = "create";
  127. setEncoding("UTF8");
  128. }
  129. /**
  130. * @ant.attribute ignore="true"
  131. */
  132. public void setWhenempty(WhenEmpty we) {
  133. log("JARs are never empty, they contain at least a manifest file",
  134. Project.MSG_WARN);
  135. }
  136. /**
  137. * @deprecated Use setDestFile(File) instead
  138. */
  139. public void setJarfile(File jarFile) {
  140. setDestFile(jarFile);
  141. }
  142. /**
  143. * Override to get hold of the original Manifest (if present and
  144. * only if updating ...
  145. *
  146. * @since Ant 1.5.2
  147. */
  148. public void setDestFile(File jarFile) {
  149. super.setDestFile(jarFile);
  150. if (jarFile.exists()) {
  151. ZipFile zf = null;
  152. try {
  153. zf = new ZipFile(jarFile);
  154. // must not use getEntry as "well behaving" applications
  155. // must accept the manifest in any capitalization
  156. Enumeration enum = zf.entries();
  157. while (enum.hasMoreElements()) {
  158. ZipEntry ze = (ZipEntry) enum.nextElement();
  159. if (ze.getName().equalsIgnoreCase("META-INF/MANIFEST.MF")) {
  160. originalManifest =
  161. getManifest(new InputStreamReader(zf
  162. .getInputStream(ze)));
  163. }
  164. }
  165. } catch (Throwable t) {
  166. log("error while reading original manifest: " + t.getMessage(),
  167. Project.MSG_WARN);
  168. } finally {
  169. if (zf != null) {
  170. try {
  171. zf.close();
  172. } catch (IOException e) {
  173. // XXX - log an error? throw an exception?
  174. }
  175. }
  176. }
  177. }
  178. }
  179. /**
  180. * Set whether or not to create an index list for classes.
  181. * This may speed up classloading in some cases.
  182. */
  183. public void setIndex(boolean flag){
  184. index = flag;
  185. }
  186. /**
  187. * Allows the manifest for the archive file to be provided inline
  188. * in the build file rather than in an external file.
  189. *
  190. * @param newManifest
  191. * @throws ManifestException
  192. */
  193. public void addConfiguredManifest(Manifest newManifest)
  194. throws ManifestException {
  195. if (configuredManifest == null) {
  196. configuredManifest = newManifest;
  197. } else {
  198. configuredManifest.merge(newManifest);
  199. }
  200. savedConfiguredManifest = configuredManifest;
  201. }
  202. /**
  203. * The manifest file to use. This can be either the location of a manifest,
  204. * or the name of a jar added through a fileset. If its the name of an added
  205. * jar, the task expects the manifest to be in the jar at META-INF/MANIFEST.MF.
  206. *
  207. * @param manifestFile
  208. */
  209. public void setManifest(File manifestFile) {
  210. if (!manifestFile.exists()) {
  211. throw new BuildException("Manifest file: " + manifestFile +
  212. " does not exist.", getLocation());
  213. }
  214. this.manifestFile = manifestFile;
  215. }
  216. private Manifest getManifest(File manifestFile) {
  217. Manifest newManifest = null;
  218. Reader r = null;
  219. try {
  220. r = new FileReader(manifestFile);
  221. newManifest = getManifest(r);
  222. } catch (IOException e) {
  223. throw new BuildException("Unable to read manifest file: "
  224. + manifestFile
  225. + " (" + e.getMessage() + ")", e);
  226. } finally {
  227. if (r != null) {
  228. try {
  229. r.close();
  230. } catch (IOException e) {
  231. // do nothing
  232. }
  233. }
  234. }
  235. return newManifest;
  236. }
  237. private Manifest getManifest(Reader r) {
  238. Manifest newManifest = null;
  239. try {
  240. newManifest = new Manifest(r);
  241. } catch (ManifestException e) {
  242. log("Manifest is invalid: " + e.getMessage(), Project.MSG_ERR);
  243. throw new BuildException("Invalid Manifest: " + manifestFile,
  244. e, getLocation());
  245. } catch (IOException e) {
  246. throw new BuildException("Unable to read manifest file"
  247. + " (" + e.getMessage() + ")", e);
  248. }
  249. return newManifest;
  250. }
  251. /**
  252. * Behavior when a Manifest is found in a zipfileset or zipgroupfileset file.
  253. * Valid values are "skip", "merge", and "mergewithoutmain".
  254. * "merge" will merge all of manifests together, and merge this into any
  255. * other specified manifests.
  256. * "mergewithoutmain" merges everything but the Main section of the manifests.
  257. * Default value is "skip".
  258. *
  259. * Note: if this attribute's value is not "skip", the created jar will not
  260. * be readable by using java.util.jar.JarInputStream
  261. *
  262. * @param config setting for found manifest behavior.
  263. */
  264. public void setFilesetmanifest(FilesetManifestConfig config) {
  265. filesetManifestConfig = config;
  266. mergeManifestsMain = "merge".equals(config.getValue());
  267. if (filesetManifestConfig != null
  268. && ! filesetManifestConfig.getValue().equals("skip")) {
  269. doubleFilePass = true;
  270. }
  271. }
  272. /**
  273. * Adds a zipfileset to include in the META-INF directory.
  274. *
  275. * @param fs zipfileset to add
  276. */
  277. public void addMetainf(ZipFileSet fs) {
  278. // We just set the prefix for this fileset, and pass it up.
  279. fs.setPrefix("META-INF/");
  280. super.addFileset(fs);
  281. }
  282. protected void initZipOutputStream(ZipOutputStream zOut)
  283. throws IOException, BuildException {
  284. if (! skipWriting) {
  285. Manifest jarManifest = createManifest();
  286. writeManifest(zOut, jarManifest);
  287. }
  288. }
  289. private Manifest createManifest()
  290. throws BuildException {
  291. try {
  292. if (!isInUpdateMode()) {
  293. originalManifest = null;
  294. }
  295. Manifest finalManifest = Manifest.getDefaultManifest();
  296. if (manifest == null) {
  297. if (manifestFile != null) {
  298. // if we haven't got the manifest yet, attempt to
  299. // get it now and have manifest be the final merge
  300. manifest = getManifest(manifestFile);
  301. }
  302. }
  303. /*
  304. * Precedence: manifestFile wins over inline manifest,
  305. * over manifests read from the filesets over the original
  306. * manifest.
  307. *
  308. * merge with null argument is a no-op
  309. */
  310. finalManifest.merge(originalManifest);
  311. finalManifest.merge(filesetManifest);
  312. finalManifest.merge(configuredManifest);
  313. finalManifest.merge(manifest, !mergeManifestsMain);
  314. return finalManifest;
  315. } catch (ManifestException e) {
  316. log("Manifest is invalid: " + e.getMessage(), Project.MSG_ERR);
  317. throw new BuildException("Invalid Manifest", e, getLocation());
  318. }
  319. }
  320. private void writeManifest(ZipOutputStream zOut, Manifest manifest)
  321. throws IOException {
  322. for (Enumeration e = manifest.getWarnings();
  323. e.hasMoreElements();) {
  324. log("Manifest warning: " + (String) e.nextElement(),
  325. Project.MSG_WARN);
  326. }
  327. zipDir(null, zOut, "META-INF/", ZipFileSet.DEFAULT_DIR_MODE);
  328. // time to write the manifest
  329. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  330. PrintWriter writer = new PrintWriter(baos);
  331. manifest.write(writer);
  332. writer.flush();
  333. ByteArrayInputStream bais =
  334. new ByteArrayInputStream(baos.toByteArray());
  335. super.zipFile(bais, zOut, "META-INF/MANIFEST.MF",
  336. System.currentTimeMillis(), null,
  337. ZipFileSet.DEFAULT_FILE_MODE);
  338. super.initZipOutputStream(zOut);
  339. }
  340. protected void finalizeZipOutputStream(ZipOutputStream zOut)
  341. throws IOException, BuildException {
  342. if (index) {
  343. createIndexList(zOut);
  344. }
  345. }
  346. /**
  347. * Create the index list to speed up classloading.
  348. * This is a JDK 1.3+ specific feature and is enabled by default. See
  349. * <a href="http://java.sun.com/j2se/1.3/docs/guide/jar/jar.html#JAR+Index">
  350. * the JAR index specification</a> for more details.
  351. *
  352. * @param zOut the zip stream representing the jar being built.
  353. * @throws IOException thrown if there is an error while creating the
  354. * index and adding it to the zip stream.
  355. */
  356. private void createIndexList(ZipOutputStream zOut) throws IOException {
  357. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  358. // encoding must be UTF8 as specified in the specs.
  359. PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos,
  360. "UTF8"));
  361. // version-info blankline
  362. writer.println("JarIndex-Version: 1.0");
  363. writer.println();
  364. // header newline
  365. writer.println(zipFile.getName());
  366. // JarIndex is sorting the directories by ascending order.
  367. // it's painful to do in JDK 1.1 and it has no value but cosmetic
  368. // since it will be read into a hashtable by the classloader.
  369. Enumeration enum = addedDirs.keys();
  370. while (enum.hasMoreElements()) {
  371. String dir = (String) enum.nextElement();
  372. // try to be smart, not to be fooled by a weird directory name
  373. // @fixme do we need to check for directories starting by ./ ?
  374. dir = dir.replace('\\', '/');
  375. int pos = dir.lastIndexOf('/');
  376. if (pos != -1){
  377. dir = dir.substring(0, pos);
  378. }
  379. // looks like nothing from META-INF should be added
  380. // and the check is not case insensitive.
  381. // see sun.misc.JarIndex
  382. if (dir.startsWith("META-INF")) {
  383. continue;
  384. }
  385. // name newline
  386. writer.println(dir);
  387. }
  388. writer.flush();
  389. ByteArrayInputStream bais =
  390. new ByteArrayInputStream(baos.toByteArray());
  391. super.zipFile(bais, zOut, INDEX_NAME, System.currentTimeMillis(), null,
  392. ZipFileSet.DEFAULT_FILE_MODE);
  393. }
  394. /**
  395. * Overriden from Zip class to deal with manifests
  396. */
  397. protected void zipFile(InputStream is, ZipOutputStream zOut, String vPath,
  398. long lastModified, File fromArchive, int mode)
  399. throws IOException {
  400. if ("META-INF/MANIFEST.MF".equalsIgnoreCase(vPath)) {
  401. if (! doubleFilePass || (doubleFilePass && skipWriting)) {
  402. filesetManifest(fromArchive, is);
  403. }
  404. } else {
  405. super.zipFile(is, zOut, vPath, lastModified, fromArchive, mode);
  406. }
  407. }
  408. private void filesetManifest(File file, InputStream is) {
  409. if (manifestFile != null && manifestFile.equals(file)) {
  410. // If this is the same name specified in 'manifest', this
  411. // is the manifest to use
  412. log("Found manifest " + file, Project.MSG_VERBOSE);
  413. if (is != null) {
  414. manifest = getManifest(new InputStreamReader(is));
  415. } else {
  416. manifest = getManifest(file);
  417. }
  418. } else if (filesetManifestConfig != null &&
  419. !filesetManifestConfig.getValue().equals("skip")) {
  420. // we add this to our group of fileset manifests
  421. log("Found manifest to merge in file " + file,
  422. Project.MSG_VERBOSE);
  423. try {
  424. Manifest newManifest = null;
  425. if (is != null) {
  426. newManifest = getManifest(new InputStreamReader(is));
  427. } else {
  428. newManifest = getManifest(file);
  429. }
  430. if (filesetManifest == null) {
  431. filesetManifest = newManifest;
  432. } else {
  433. filesetManifest.merge(newManifest);
  434. }
  435. } catch (ManifestException e) {
  436. log("Manifest in file " + file + " is invalid: "
  437. + e.getMessage(), Project.MSG_ERR);
  438. throw new BuildException("Invalid Manifest", e, getLocation());
  439. }
  440. } else {
  441. // assuming 'skip' otherwise
  442. // don't warn if skip has been requested explicitly, warn if user
  443. // didn't set the attribute
  444. // Hide warning also as it makes no sense since
  445. // the filesetmanifest attribute itself has been
  446. // hidden
  447. //int logLevel = filesetManifestConfig == null ?
  448. // Project.MSG_WARN : Project.MSG_VERBOSE;
  449. //log("File " + file
  450. // + " includes a META-INF/MANIFEST.MF which will be ignored. "
  451. // + "To include this file, set filesetManifest to a value other "
  452. // + "than 'skip'.", logLevel);
  453. }
  454. }
  455. /**
  456. * Collect the resources that are newer than the corresponding
  457. * entries (or missing) in the original archive.
  458. *
  459. * <p>If we are going to recreate the archive instead of updating
  460. * it, all resources should be considered as new, if a single one
  461. * is. Because of this, subclasses overriding this method must
  462. * call <code>super.getResourcesToAdd</code> and indicate with the
  463. * third arg if they already know that the archive is
  464. * out-of-date.</p>
  465. *
  466. * @param filesets The filesets to grab resources from
  467. * @param zipFile intended archive file (may or may not exist)
  468. * @param needsUpdate whether we already know that the archive is
  469. * out-of-date. Subclasses overriding this method are supposed to
  470. * set this value correctly in their call to
  471. * super.getResourcesToAdd.
  472. * @return an array of resources to add for each fileset passed in.
  473. *
  474. * @exception BuildException if it likes
  475. */
  476. protected Resource[][] getResourcesToAdd(FileSet[] filesets,
  477. File zipFile,
  478. boolean needsUpdate)
  479. throws BuildException {
  480. // need to handle manifest as a special check
  481. if (configuredManifest != null || manifestFile == null) {
  482. java.util.zip.ZipFile theZipFile = null;
  483. try {
  484. theZipFile = new java.util.zip.ZipFile(zipFile);
  485. java.util.zip.ZipEntry entry =
  486. theZipFile.getEntry("META-INF/MANIFEST.MF");
  487. if (entry == null) {
  488. log("Updating jar since the current jar has no manifest",
  489. Project.MSG_VERBOSE);
  490. needsUpdate = true;
  491. } else {
  492. Manifest currentManifest =
  493. new Manifest(new InputStreamReader(theZipFile
  494. .getInputStream(entry)));
  495. Manifest newManifest = createManifest();
  496. if (!currentManifest.equals(newManifest)) {
  497. log("Updating jar since jar manifest has changed",
  498. Project.MSG_VERBOSE);
  499. needsUpdate = true;
  500. }
  501. }
  502. } catch (Exception e) {
  503. // any problems and we will rebuild
  504. log("Updating jar since cannot read current jar manifest: "
  505. + e.getClass().getName() + " - " + e.getMessage(),
  506. Project.MSG_VERBOSE);
  507. needsUpdate = true;
  508. } finally {
  509. if (theZipFile != null) {
  510. try {
  511. theZipFile.close();
  512. } catch (IOException e) {
  513. //ignore
  514. }
  515. }
  516. }
  517. } else if (manifestFile.lastModified() > zipFile.lastModified()) {
  518. log("Updating jar since manifestFile is newer than the archive",
  519. Project.MSG_VERBOSE);
  520. needsUpdate = true;
  521. }
  522. Resource[][] fromZip =
  523. super.getResourcesToAdd(filesets, zipFile, needsUpdate);
  524. if (needsUpdate && isEmpty(fromZip)) {
  525. // archive doesn't have any content apart from the manifest
  526. /*
  527. * OK, this is a hack.
  528. *
  529. * Zip doesn't care if the array we return is longer than
  530. * the array of filesets, so we can savely append an
  531. * additional non-empty array. This will make Zip think
  532. * that there are resources out-of-date and at the same
  533. * time add nothing.
  534. *
  535. * The whole manifest handling happens in initZipOutputStream.
  536. */
  537. Resource[][] tmp = new Resource[fromZip.length + 1][];
  538. System.arraycopy(fromZip, 0, tmp, 0, fromZip.length);
  539. tmp[fromZip.length] = new Resource[] {new Resource("")};
  540. fromZip = tmp;
  541. }
  542. return fromZip;
  543. }
  544. protected boolean createEmptyZip(File zipFile) {
  545. // Jar files always contain a manifest and can never be empty
  546. return true;
  547. }
  548. /**
  549. * Make sure we don't think we already have a MANIFEST next time this task
  550. * gets executed.
  551. *
  552. * @see Zip#cleanUp
  553. */
  554. protected void cleanUp() {
  555. super.cleanUp();
  556. // we want to save this info if we are going to make another pass
  557. if (! doubleFilePass || (doubleFilePass && ! skipWriting))
  558. {
  559. manifest = null;
  560. configuredManifest = savedConfiguredManifest;
  561. filesetManifest = null;
  562. originalManifest = null;
  563. }
  564. }
  565. /**
  566. * reset to default values.
  567. *
  568. * @see Zip#reset
  569. *
  570. * @since 1.44, Ant 1.5
  571. */
  572. public void reset() {
  573. super.reset();
  574. configuredManifest = null;
  575. filesetManifestConfig = null;
  576. mergeManifestsMain = false;
  577. manifestFile = null;
  578. index = false;
  579. }
  580. public static class FilesetManifestConfig extends EnumeratedAttribute {
  581. public String[] getValues() {
  582. return new String[] {"skip", "merge", "mergewithoutmain"};
  583. }
  584. }
  585. }