packpanda.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. #############################################################################
  2. #
  3. # packpanda - this is a tool that packages up a panda game into a
  4. # convenient, easily-downloaded windows executable. Packpanda runs on linux
  5. # and windows - on linux, it builds .debs and .rpms, on windows it relies on
  6. # NSIS, the nullsoft scriptable install system, to do the hard work.
  7. #
  8. # This is intentionally a very simplistic game-packer with very
  9. # limited options. The goal is simplicity, not feature richness.
  10. # There are dozens of complex, powerful packaging tools already out
  11. # there. This one is for people who just want to do it quick and
  12. # easy.
  13. #
  14. ##############################################################################
  15. import sys, os, getopt, shutil, py_compile, subprocess
  16. OPTIONLIST = [
  17. ("dir", 1, "Name of directory containing game"),
  18. ("name", 1, "Human-readable name of the game"),
  19. ("version", 1, "Version number to add to game name"),
  20. ("rmdir", 2, "Delete all directories with given name"),
  21. ("rmext", 2, "Delete all files with given extension"),
  22. ("fast", 0, "Use fast compression instead of good compression"),
  23. ("bam", 0, "Generate BAM files, change default-model-extension to BAM"),
  24. ("pyc", 0, "Generate PYC files"),
  25. ]
  26. def ParseFailure():
  27. print("")
  28. print("packpanda usage:")
  29. print("")
  30. for (opt, hasval, explanation) in OPTIONLIST:
  31. if (hasval):
  32. print(" --%-10s %s"%(opt+" x", explanation))
  33. else:
  34. print(" --%-10s %s"%(opt+" ", explanation))
  35. sys.exit(1)
  36. def ParseOptions(args):
  37. try:
  38. options = {}
  39. longopts = []
  40. for (opt, hasval, explanation) in OPTIONLIST:
  41. if (hasval==2):
  42. longopts.append(opt+"=")
  43. options[opt] = []
  44. elif (hasval==1):
  45. longopts.append(opt+"=")
  46. options[opt] = ""
  47. else:
  48. longopts.append(opt)
  49. options[opt] = 0
  50. opts, extras = getopt.getopt(args, "", longopts)
  51. for option, value in opts:
  52. for (opt, hasval, explanation) in OPTIONLIST:
  53. if (option == "--"+opt):
  54. if (hasval==2): options[opt].append(value)
  55. elif (hasval==1): options[opt] = value
  56. else: options[opt] = 1
  57. return options
  58. except: ParseFailure();
  59. OPTIONS = ParseOptions(sys.argv[1:])
  60. ##############################################################################
  61. #
  62. # Locate the relevant trees.
  63. #
  64. ##############################################################################
  65. PANDA=None
  66. for dir in sys.path:
  67. if (dir != "") and os.path.exists(os.path.join(dir,"direct")) and os.path.exists(os.path.join(dir,"pandac")):
  68. PANDA=os.path.abspath(dir)
  69. if (PANDA is None):
  70. sys.exit("Cannot locate the panda root directory in the python path (cannot locate directory containing direct and pandac).")
  71. print("PANDA located at "+PANDA)
  72. if (os.path.exists(os.path.join(PANDA,"..","makepanda","makepanda.py"))) and (sys.platform != "win32" or os.path.exists(os.path.join(PANDA,"..","thirdparty","win-nsis","makensis.exe"))):
  73. PSOURCE=os.path.abspath(os.path.join(PANDA,".."))
  74. if (sys.platform == "win32"):
  75. NSIS=os.path.abspath(os.path.join(PANDA,"..","thirdparty","win-nsis"))
  76. else:
  77. PSOURCE=PANDA
  78. if (sys.platform == "win32"):
  79. NSIS=os.path.join(PANDA,"nsis")
  80. ##############################################################################
  81. #
  82. # Identify the main parts of the game: DIR, NAME, MAIN, ICON, BITMAP, etc
  83. #
  84. ##############################################################################
  85. VER=OPTIONS["version"]
  86. DIR=OPTIONS["dir"]
  87. if (DIR==""):
  88. print("You must specify the --dir option.")
  89. ParseFailure()
  90. DIR=os.path.abspath(DIR)
  91. MYDIR=os.path.abspath(os.getcwd())
  92. BASENAME=os.path.basename(DIR)
  93. if (OPTIONS["name"] != ""):
  94. NAME=OPTIONS["name"]
  95. else:
  96. NAME=BASENAME
  97. SMDIRECTORY=NAME
  98. if (VER!=""): SMDIRECTORY=SMDIRECTORY+" "+VER
  99. PYTHONV="python"+sys.version[:3]
  100. LICENSE=os.path.join(DIR, "license.txt")
  101. OUTFILE=os.path.basename(DIR)
  102. if (VER!=""): OUTFILE=OUTFILE+"-"+VER
  103. if (sys.platform == "win32"):
  104. ICON=os.path.join(DIR, "icon.ico")
  105. BITMAP=os.path.join(DIR, "installer.bmp")
  106. OUTFILE=os.path.abspath(OUTFILE+".exe")
  107. INSTALLDIR='C:\\'+os.path.basename(DIR)
  108. if (VER!=""): INSTALLDIR=INSTALLDIR+"-"+VER
  109. COMPRESS="lzma"
  110. if (OPTIONS["fast"]): COMPRESS="zlib"
  111. if (OPTIONS["pyc"]): MAIN="main.pyc"
  112. else: MAIN="main.py"
  113. def PrintFileStatus(label, file):
  114. if (os.path.exists(file)):
  115. print("%-15s: %s"%(label, file))
  116. else:
  117. print("%-15s: %s (MISSING)"%(label, file))
  118. PrintFileStatus("Dir", DIR)
  119. print("%-15s: %s"%("Name", NAME))
  120. print("%-15s: %s"%("Start Menu", SMDIRECTORY))
  121. PrintFileStatus("Main", os.path.join(DIR, MAIN))
  122. if (sys.platform == "win32"):
  123. PrintFileStatus("Icon", ICON)
  124. PrintFileStatus("Bitmap", BITMAP)
  125. PrintFileStatus("License", LICENSE)
  126. print("%-15s: %s"%("Output", OUTFILE))
  127. if (sys.platform == "win32"):
  128. print("%-15s: %s"%("Install Dir", INSTALLDIR))
  129. if (os.path.isdir(DIR)==0):
  130. sys.exit("Difficulty reading "+DIR+". Cannot continue.")
  131. if (os.path.isfile(os.path.join(DIR, "main.py"))==0):
  132. sys.exit("Difficulty reading main.py. Cannot continue.")
  133. if (os.path.isfile(LICENSE)==0):
  134. LICENSE=os.path.join(PANDA,"LICENSE")
  135. if (sys.platform == "win32") and (os.path.isfile(BITMAP)==0):
  136. BITMAP=os.path.join(NSIS,"Contrib","Graphics","Wizard","nsis.bmp")
  137. if (sys.platform == "win32"):
  138. if (os.path.isfile(ICON)==0):
  139. PPICON="bin\\ppython.exe"
  140. else:
  141. PPICON="game\\icon.ico"
  142. ##############################################################################
  143. #
  144. # Copy the game to a temporary directory, so we can modify it safely.
  145. #
  146. ##############################################################################
  147. def limitedCopyTree(src, dst, rmdir):
  148. if (os.path.isdir(src)):
  149. if (os.path.basename(src) in rmdir):
  150. return
  151. if (not os.path.isdir(dst)): os.mkdir(dst)
  152. for x in os.listdir(src):
  153. limitedCopyTree(os.path.join(src,x), os.path.join(dst,x), rmdir)
  154. else:
  155. shutil.copyfile(src, dst)
  156. TMPDIR=os.path.abspath("packpanda-TMP")
  157. if (sys.platform == "win32"):
  158. TMPGAME=os.path.join(TMPDIR,"game")
  159. TMPETC=os.path.join(TMPDIR,"etc")
  160. else:
  161. TMPGAME=os.path.join(TMPDIR,"usr","share","games",BASENAME,"game")
  162. TMPETC=os.path.join(TMPDIR,"usr","share","games",BASENAME,"etc")
  163. print("")
  164. print("Copying the game to "+TMPDIR+"...")
  165. if (os.path.exists(TMPDIR)):
  166. try: shutil.rmtree(TMPDIR)
  167. except: sys.exit("Cannot delete "+TMPDIR)
  168. try:
  169. os.mkdir(TMPDIR)
  170. rmdir = {}
  171. for x in OPTIONS["rmdir"]:
  172. rmdir[x] = 1
  173. if not os.path.isdir( TMPGAME ):
  174. os.makedirs(TMPGAME)
  175. limitedCopyTree(DIR, TMPGAME, rmdir)
  176. if not os.path.isdir( TMPETC ):
  177. os.makedirs(TMPETC)
  178. if sys.platform == "win32":
  179. limitedCopyTree(os.path.join(PANDA, "etc"), TMPETC, {})
  180. else:
  181. shutil.copyfile("/etc/Config.prc", os.path.join(TMPETC, "Config.prc"))
  182. shutil.copyfile("/etc/Confauto.prc", os.path.join(TMPETC, "Confauto.prc"))
  183. except: sys.exit("Cannot copy game to "+TMPDIR)
  184. ##############################################################################
  185. #
  186. # If --bam requested, change default-model-extension .egg to bam.
  187. #
  188. ##############################################################################
  189. def ReadFile(wfile):
  190. try:
  191. srchandle = open(wfile, "rb")
  192. data = srchandle.read()
  193. srchandle.close()
  194. return data
  195. except: exit("Cannot read "+wfile)
  196. def WriteFile(wfile,data):
  197. try:
  198. dsthandle = open(wfile, "wb")
  199. dsthandle.write(data)
  200. dsthandle.close()
  201. except: exit("Cannot write "+wfile)
  202. if OPTIONS["bam"]:
  203. CONF=ReadFile(os.path.join(TMPETC,"Confauto.prc"))
  204. CONF=CONF.replace("default-model-extension .egg","default-model-extension .bam")
  205. WriteFile(os.path.join(TMPETC,"Confauto.prc"), CONF)
  206. ##############################################################################
  207. #
  208. # Compile all py files, convert all egg files.
  209. #
  210. # We do this as a sanity check, even if the user
  211. # hasn't requested that his files be compiled.
  212. #
  213. ##############################################################################
  214. if (sys.platform == "win32"):
  215. EGG2BAM=os.path.join(PANDA,"bin","egg2bam.exe")
  216. else:
  217. EGG2BAM=os.path.join(PANDA,"bin","egg2bam")
  218. def egg2bam(file,bam):
  219. present = os.path.exists(bam)
  220. if (present): bam = "packpanda-TMP.bam";
  221. cmd = 'egg2bam -noabs -ps rel -pd . "'+file+'" -o "'+bam+'"'
  222. print("Executing: "+cmd)
  223. if (sys.platform == "win32"):
  224. res = os.spawnl(os.P_WAIT, EGG2BAM, cmd)
  225. else:
  226. res = os.system(cmd)
  227. if (res != 0): sys.exit("Problem in egg file: "+file)
  228. if (present) or (OPTIONS["bam"]==0):
  229. os.unlink(bam)
  230. def py2pyc(file):
  231. print("Compiling python "+file)
  232. pyc = file[:-3]+'.pyc'
  233. pyo = file[:-3]+'.pyo'
  234. if (os.path.exists(pyc)): os.unlink(pyc)
  235. if (os.path.exists(pyo)): os.unlink(pyo)
  236. try: py_compile.compile(file)
  237. except: sys.exit("Cannot compile "+file)
  238. if (OPTIONS["pyc"]==0):
  239. if (os.path.exists(pyc)):
  240. os.unlink(pyc)
  241. if (os.path.exists(pyo)):
  242. os.unlink(pyo)
  243. def CompileFiles(file):
  244. if (os.path.isfile(file)):
  245. if (file.endswith(".egg")):
  246. egg2bam(file, file[:-4]+'.bam')
  247. elif (file.endswith(".egg.pz") or file.endswith(".egg.gz")):
  248. egg2bam(file, file[:-7]+'.bam')
  249. elif (file.endswith(".py")):
  250. py2pyc(file)
  251. else: pass
  252. elif (os.path.isdir(file)):
  253. for x in os.listdir(file):
  254. CompileFiles(os.path.join(file, x))
  255. def DeleteFiles(file):
  256. base = os.path.basename(file).lower()
  257. if (os.path.isdir(file)):
  258. for pattern in OPTIONS["rmdir"]:
  259. if pattern.lower() == base:
  260. print("Deleting "+file)
  261. shutil.rmtree(file)
  262. return
  263. for x in os.listdir(file):
  264. DeleteFiles(os.path.join(file, x))
  265. else:
  266. for ext in OPTIONS["rmext"]:
  267. if base[-(len(ext) + 1):] == ("." + ext).lower():
  268. print("Deleting "+file)
  269. os.unlink(file)
  270. return
  271. print("")
  272. print("Compiling BAM and PYC files...")
  273. os.chdir(TMPGAME)
  274. CompileFiles(".")
  275. DeleteFiles(".")
  276. ##############################################################################
  277. #
  278. # Now make the installer. Yay!
  279. #
  280. ##############################################################################
  281. INSTALLER_DEB_FILE="""
  282. Package: BASENAME
  283. Version: VERSION
  284. Section: games
  285. Priority: optional
  286. Architecture: ARCH
  287. Essential: no
  288. Depends: PYTHONV
  289. Provides: BASENAME
  290. Description: NAME
  291. Maintainer: Unknown
  292. """
  293. INSTALLER_SPEC_FILE="""
  294. Summary: NAME
  295. Name: BASENAME
  296. Version: VERSION
  297. Release: 1
  298. Group: Amusement/Games
  299. License: See license file
  300. BuildRoot: TMPDIR
  301. BuildRequires: PYTHONV
  302. %description
  303. NAME
  304. %files
  305. %defattr(-,root,root)
  306. /usr/bin/BASENAME
  307. /usr/lib/games/BASENAME
  308. /usr/share/games/BASENAME
  309. """
  310. RUN_SCRIPT="""
  311. #!/bin/sh
  312. cd /usr/share/games/BASENAME/game
  313. PYTHONPATH=/usr/lib/games/BASENAME:/usr/share/games/BASENAME
  314. LD_LIBRARY_PATH=/usr/lib/games/BASENAME
  315. PYTHONV MAIN
  316. """
  317. if (sys.platform == "win32"):
  318. CMD="\""+NSIS+"\\makensis.exe\" /V2 "
  319. CMD=CMD+'/DCOMPRESSOR="'+COMPRESS+'" '
  320. CMD=CMD+'/DNAME="'+NAME+'" '
  321. CMD=CMD+'/DSMDIRECTORY="'+SMDIRECTORY+'" '
  322. CMD=CMD+'/DINSTALLDIR="'+INSTALLDIR+'" '
  323. CMD=CMD+'/DOUTFILE="'+OUTFILE+'" '
  324. CMD=CMD+'/DLICENSE="'+LICENSE+'" '
  325. CMD=CMD+'/DLANGUAGE="English" '
  326. CMD=CMD+'/DRUNTEXT="Play '+NAME+'" '
  327. CMD=CMD+'/DIBITMAP="'+BITMAP+'" '
  328. CMD=CMD+'/DUBITMAP="'+BITMAP+'" '
  329. CMD=CMD+'/DPANDA="'+PANDA+'" '
  330. CMD=CMD+'/DPANDACONF="'+TMPETC+'" '
  331. CMD=CMD+'/DPSOURCE="'+PSOURCE+'" '
  332. CMD=CMD+'/DPPGAME="'+TMPGAME+'" '
  333. CMD=CMD+'/DPPMAIN="'+MAIN+'" '
  334. CMD=CMD+'/DPPICON="'+PPICON+'" '
  335. CMD=CMD+'"'+PSOURCE+'\\direct\\directscripts\\packpanda.nsi"'
  336. print("")
  337. print(CMD)
  338. print("packing...")
  339. subprocess.call(CMD)
  340. else:
  341. os.chdir(MYDIR)
  342. os.system("mkdir -p %s/usr/bin" % TMPDIR)
  343. os.system("mkdir -p %s/usr/share/games/%s" % (TMPDIR, BASENAME))
  344. os.system("mkdir -p %s/usr/lib/games/%s" % (TMPDIR, BASENAME))
  345. os.system("cp --recursive %s/direct %s/usr/share/games/%s/direct" % (PANDA, TMPDIR, BASENAME))
  346. os.system("cp --recursive %s/pandac %s/usr/share/games/%s/pandac" % (PANDA, TMPDIR, BASENAME))
  347. os.system("cp --recursive %s/models %s/usr/share/games/%s/models" % (PANDA, TMPDIR, BASENAME))
  348. os.system("cp --recursive %s/Pmw %s/usr/share/games/%s/Pmw" % (PANDA, TMPDIR, BASENAME))
  349. os.system("cp %s %s/usr/share/games/%s/LICENSE" % (LICENSE, TMPDIR, BASENAME))
  350. os.system("cp --recursive /usr/lib/panda3d/* %s/usr/lib/games/%s/" % (TMPDIR, BASENAME))
  351. # Make the script to run the game
  352. txt = RUN_SCRIPT[1:].replace("BASENAME",BASENAME).replace("PYTHONV",PYTHONV).replace("MAIN",MAIN)
  353. WriteFile(TMPDIR+"/usr/bin/"+BASENAME, txt)
  354. os.system("chmod +x "+TMPDIR+"/usr/bin/"+BASENAME)
  355. if (os.path.exists("/usr/bin/rpmbuild")):
  356. os.system("rm -rf %s/DEBIAN" % TMPDIR)
  357. os.system("rpm -E '%_target_cpu' > packpanda-TMP.txt")
  358. ARCH=ReadFile("packpanda-TMP.txt").strip()
  359. os.remove("packpanda-TMP.txt")
  360. txt = INSTALLER_SPEC_FILE[1:].replace("VERSION",VER).replace("TMPDIR",TMPDIR)
  361. txt = txt.replace("BASENAME",BASENAME).replace("NAME",NAME).replace("PYTHONV",PYTHONV)
  362. WriteFile("packpanda-TMP.spec", txt)
  363. os.system("rpmbuild --define '_rpmdir "+TMPDIR+"' -bb packpanda-TMP.spec")
  364. os.system("mv "+ARCH+"/"+BASENAME+"-"+VER+"-1."+ARCH+".rpm .")
  365. os.rmdir(ARCH)
  366. os.remove("packpanda-TMP.spec")
  367. if (os.path.exists("/usr/bin/dpkg-deb")):
  368. os.system("dpkg --print-architecture > packpanda-TMP.txt")
  369. ARCH=ReadFile("packpanda-TMP.txt").strip()
  370. os.remove("packpanda-TMP.txt")
  371. txt = INSTALLER_DEB_FILE[1:].replace("VERSION",str(VER)).replace("PYTHONV",PYTHONV)
  372. txt = txt.replace("BASENAME",BASENAME).replace("NAME",NAME).replace("ARCH",ARCH)
  373. os.system("mkdir -p %s/DEBIAN" % TMPDIR)
  374. os.system("cd %s ; (find usr -type f -exec md5sum {} \;) > DEBIAN/md5sums" % TMPDIR)
  375. WriteFile(TMPDIR+"/DEBIAN/control",txt)
  376. os.system("dpkg-deb -b "+TMPDIR+" "+BASENAME+"_"+VER+"_"+ARCH+".deb")
  377. if not(os.path.exists("/usr/bin/rpmbuild") or os.path.exists("/usr/bin/dpkg-deb")):
  378. exit("To build an installer, either rpmbuild or dpkg-deb must be present on your system!")