├── .github
├── SETUP.md
├── dependabot.yml
└── workflows
│ └── e2e.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── command_line.py
├── config.py
├── files
├── base_url.txt
├── compressors
│ ├── upx-linux-x32
│ ├── upx-linux-x64
│ ├── upx-mac
│ └── upx-win.exe
├── env_vars.bash
├── env_vars.bat
├── env_vars.py
├── images
│ ├── app_settings.png
│ ├── app_settings.svg
│ ├── compress_settings.png
│ ├── compress_settings.svg
│ ├── download_settings.png
│ ├── download_settings.svg
│ ├── export_settings.png
│ ├── export_settings.svg
│ ├── folder_open.png
│ ├── icon.png
│ ├── icon.svg
│ ├── warning.png
│ ├── warning.svg
│ ├── window_settings.png
│ └── window_settings.svg
├── nw-versions.txt
├── settings.cfg
└── version.txt
├── image_utils
├── __init__.py
├── icns_info.py
├── image_utils.py
├── png.py
└── pycns.py
├── images
├── icon.icns
├── icon.ico
└── icon.png
├── main.py
├── pe.py
├── requirements.txt
├── scripts
├── Info.plist
├── Web2Exe.nsi
├── build_command_line_linux.bash
├── build_mac.bash
├── build_windows.bat
└── upload_release.py
├── tests
├── __init__.py
├── conftest.py
└── test_command_line.py
├── util_classes.py
└── utils.py
/.github/SETUP.md:
--------------------------------------------------------------------------------
1 | #Setup Guide
2 |
3 | In order to run Web2Executable from Python code to make modifications to this repo, you'll need to install a few prerequisites first.
4 |
5 | ##Prerequisites
6 |
7 | ###Qt 4.8.7
8 |
9 | Download and install Qt 4.8.7 from [here](https://download.qt.io/official_releases/qt/4.8/4.8.7/) or from your package manager. PySide uses Qt 4.8.X and is incompatible with Qt 5 and higher.
10 |
11 | For Mac OSX, it might be needed to install the latest 4.8.X version via compiling the source. Download the `qt-everywhere-opensource-src-4.8.X` version and run the following in the extracted directory:
12 |
13 | ```
14 | ./configure
15 | make
16 | sudo make install
17 | ```
18 |
19 | Alternatively, you may wish to install it via homebrew if you have it installed:
20 |
21 | ```
22 | brew install qt
23 | ```
24 |
25 | ###Python 3.4
26 |
27 | Download and install the latest Python3.4.X release from the Python website. Python 3.5 and higher may work, but this repo was only tested with Python 3.4.
28 |
29 | ###System requirements
30 |
31 | If you want to use the conversion of any image to icns and png using Pillow, you must install some libraries.
32 |
33 | libjpeg, libpng, openjpeg
34 |
35 | ###Pip Requirements
36 |
37 | Install pip requirements with
38 |
39 | ```
40 | pip install -r requirements.txt
41 | ```
42 |
43 | ##Running
44 |
45 | Once all of the above are installed, simply run:
46 |
47 | ```
48 | python3.4 main.py
49 | ```
50 |
51 | ##Tests
52 |
53 | Tests are located in the tests directory and were created with pytest.
54 | They can be run with:
55 |
56 | ```
57 | pytest
58 | ```
59 |
60 | in the root directory.
61 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "pip"
4 | directory: "."
5 | schedule:
6 | interval: "weekly"
7 | day: "saturday"
8 | groups:
9 | pip:
10 | patterns:
11 | - "*"
12 | - package-ecosystem: "github-actions"
13 | directory: ".github/"
14 | schedule:
15 | interval: "weekly"
16 | day: "saturday"
17 | groups:
18 | gha:
19 | patterns:
20 | - "*"
21 |
--------------------------------------------------------------------------------
/.github/workflows/e2e.yml:
--------------------------------------------------------------------------------
1 | name: e2e
2 |
3 | on:
4 | pull_request:
5 | branches: [master]
6 |
7 | concurrency:
8 | group: ${{ github.ref }}
9 | cancel-in-progress: true
10 |
11 | jobs:
12 | e2e:
13 | strategy:
14 | matrix:
15 | os: [macos-14, ubuntu-22.04, windows-2022]
16 | fail-fast: false
17 | runs-on: ${{ matrix.os }}
18 | steps:
19 | - uses: actions/checkout@v3.3.0
20 | - uses: actions/setup-python@v4.6.1
21 | with:
22 | python-version: '3.12.0'
23 | cache: pip
24 | - if: matrix.os == 'ubuntu-22.04'
25 | uses: awalsh128/cache-apt-pkgs-action@v1.3.0
26 | with:
27 | packages: libegl1
28 | version: 1.0
29 | - run: python -m pip install -r requirements.txt
30 | # - run: python -m pylint ./**/*.py
31 | - run: python -m pytest
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.sw*
3 | *.spec
4 |
5 | tests/test_data
6 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # "No Ideologies" Code of Conduct
2 |
3 | The following are the guidelines we expect our community members and maintainers to follow.
4 |
5 | * * *
6 |
7 | ## Terminology and Scope
8 |
9 | **What defines a "maintainer"?**
10 |
11 | * A maintainer is anyone that interacts with the community on behalf of this project. Amount of code written is not a qualifier. A maintainer may include those who solely help in support roles such as in resolving issues, improving documentation, administrating or moderating forums/chatrooms, or any other non-coding specific roles. Maintainers also include those that are responsible for the building and upkeep of the project.
12 |
13 | **What defines a "community member"?**
14 |
15 | * Anyone interacting with this project directly, including maintainers.
16 |
17 | **What is the scope of these guidelines?**
18 |
19 | * These guidelines apply only to this project and forms of communication directly related to it, such as issue trackers, forums, chatrooms, and in person events specific to this project. If a member is violating these guidelines outside of this project or on other platforms, that is beyond our scope and any grievances should be handled on those platforms.
20 |
21 | **Discussing the guidelines:**
22 |
23 | * Discussions around these guidelines, improving, updating, or altering them, is permitted so long as the discussions do not violate any existing guidelines.
24 |
25 | * * *
26 |
27 | ## Guidelines
28 |
29 | ### Guidelines for community members
30 |
31 | This project is technical in nature and not based around any particular non-technical ideology. As such, communication that is based primarily around ideologies unrelated to the technologies used by this repository are not permitted.
32 |
33 | Any discussion or communication that is primarily focused around an ideology, be it about race, gender, politics, religion, or anything else non-technical, is not allowed. Everyone has their own ideological preferences, beliefs, and opinions. We do not seek to marginalize, exclude, or judge anyone for their ideologies. To prevent conflict between those with differing or opposing ideologies, all communication on these subjects are prohibited. Some discussions around these topics may be important, however this project is not the proper channel for these discussions.
34 |
35 | ### Guidelines for maintainers
36 |
37 | * Maintainers must abide by the same rules as all other community members mentioned above. However, in addition, maintainers are held to a higher standard, explained below.
38 | * Maintainers should answer all questions politely.
39 | * If someone is upset or angry about something, it's probably because it's difficult to use, so thank them for bringing it to your attention and address ways to solve the problem. Maintainers should focus on the content of the message, and not on how it was delivered.
40 | * A maintainer should seek to update members when an issue they brought up is resolved.
41 |
42 | * * *
43 |
44 | ## Appropriate response to violations
45 |
46 | How to respond to a community member or maintainer violating a guideline.
47 |
48 | 1. If an issue is created that violates a guideline a maintainer should close and lock the issue, explaining "This issue is in violation of our code of conduct. Please review it before posting again." with a link to this document.
49 | 1. If a member repeatedly violates the guidelines established in this document, they should be politely warned that continuing to violate the rules may result in being banned from the community. This means revoking access and support to interactions relating directly to the project (issue trackers, chatrooms, forums, in person events, etc.). However, they may continue to use the technology in accordance with its license.
50 | 1. If a maintainer is in violation of a guideline, they should be informed of such with a link to this document. If additional actions are required of the maintainer but not taken, then other maintainers should be informed of these inactions.
51 | 1. If a maintainer repeatedly violates the guidelines established in this document, they should be politely warned that continuing to violate the rules may result in being banned from the community. This means revoking access and support to interactions relating directly to the project (issue trackers, chatrooms, forums, in person events, etc.). However, they may continue to use the technology in accordance with its license. In addition, future contributions to this project may be ignored as well.
52 |
53 | * * *
54 |
55 | Based on version 1.0.3 from https://github.com/CodifiedConduct/coc-no-ideologies
56 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2023 SimplyPixelated
4 | Copyright (c) 2022-2023 NW.js Utilities
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > This repository is in maintainance mode. Bug fixes will be provided on a best effort basis. If you use this project, please consider contributing back.
2 |
3 | Web2Executable
4 | ==============
5 |
6 | []()
7 |
8 | [Releases (Downloads)](https://github.com/jyapayne/Web2Executable/releases) (new!)
9 |
10 | []()
11 |
12 | What is it?
13 | -----------
14 |
15 | Web2Executable is a friendly command line and GUI application that can transform your Nodejs (or any other JS/HTML) app into an executable (and libraries) that can run in a contained, desktop-like style. It can export to Mac OS X, Windows and Linux all from one platform, so no need to go out and buy expensive hardware.
16 |
17 | It's powered by the very awesome project [NWJS](https://github.com/nwjs) and PySide, is open source, and is just dang awesome and easy to use.
18 |
19 | If you have an idea for a feature, please create a new issue with a format like this: "Feature - My Awesome New Feature", along with a good description of what you'd like the feature to do.
20 |
21 | On the other hand, if you have any annoyances with the application and want to contribute to making it better for everyone, please file an issue with "Annoyance:" as the first part of the title. Sometimes it's hard to know what is annoying for people and input is much appreciated :)
22 |
23 | What About Electron?
24 | --------------------
25 |
26 | If you want to export using Electron instead of NW.js, try [Electrify](https://github.com/jyapayne/Electrify), my other app based on Web2Executable.
27 |
28 |
29 | Who's Using It?
30 | ---------------
31 |
32 | Lots of people! There are currently thousands of downloads and several articles written about using Web2Executable.
33 |
34 | Some articles include:
35 |
36 | [Getting a Phaser Game on Steam](http://phaser.io/news/2015/10/getting-a-phaser-game-on-steam)
37 |
38 | [Packt Publishing NW.js Essentials Tutorial](https://www.packtpub.com/packtlib/book/Web-Development/9781785280863/7/ch07lvl1sec53/Web2Executable) and [Ebook](https://books.google.ca/books?id=wz6qCQAAQBAJ&pg=PA135&lpg=PA135&dq=web2executable&source=bl&ots=sPP-3BOMXX&sig=UolyF31WcTgA-lrel2UTIfzs65U&hl=en&sa=X&redir_esc=y#v=onepage&q=web2executable&f=false)
39 |
40 | [A Russian NW.js Tutoral](http://canonium.com/articles/nwjs-web-to-executable)
41 |
42 | [Marv's Blog](http://www.marv.ph/tag/web2exe/)
43 |
44 | [Shotten.com Node-webkit for Poets](http://www.shotton.com/wp/2014/10/27/node-webkit-for-poets-mac-version/)
45 |
46 | If you have a project you'd like to see listed here that was successfully built using Web2Executable or you have written an article that mentions it, feel free to send me an email with a link and I'd be super stoked to paste it here :)
47 |
48 |
49 | Features
50 | --------
51 |
52 | - Cross platform to Mac, Windows, Linux
53 | - Working media out of the box (sound and video)
54 | - Easy to use and straightforward
55 | - Streamlined workflow from project -> working standalone exe
56 | - Same performance as Google Chrome
57 | - Works with Phaser; should work with other HTML5 game libraries
58 | - Export web applications to all platforms from your current OS
59 | - Ability to specify a node-webkit version to download
60 | - Automatic insertion of icon files into Windows exe's or Mac Apps by filling out the icon fields as necessary
61 | - A command line utility with functionality equivalent to the GUI. Useful for automated builds.
62 | - Compression of executables with upx
63 |
64 | Planned New Features
65 | --------------------
66 |
67 | - The ability to add external files to the project
68 | - Minifying JS and HTML
69 |
70 |
71 | Getting Started
72 | ---------------
73 |
74 | ### Using Prebuilt Binaries
75 |
76 | When using the prebuilt binaries for Windows, Mac, or Ubuntu, there are NO dependencies or extra applications needed. Simply download Web2Exe for the platform of your choice, extract, and double click the app/exe/binary to start. This applies to both the command-line version and the GUI version.
77 |
78 | **NOTE!**: Some people report needing the Microsoft Visual C++ 2008/2010 SP1 and regular Redistributable package. I don't have a machine to test this, but just know that you might need the package if the application won't open or spits out the following error:
79 |
80 | ```
81 | Error: The application has failed to start because the side by side configuration is incorrect please see the application event log or use the command line sxstrace.exe tool for more detail.
82 | ```
83 |
84 |
85 | ### Building from Source
86 |
87 | Requires the PySide library and Python 3.4.3 or higher. If you want to replace the icon in the Windows Exe's, this will do it automatically with the latest code if you have PIL or Pillow installed.
88 |
89 | ### Command line interface
90 |
91 | Dependencies: configobj (install with pip) and Pillow if you want icon replacement (not necessary)
92 |
93 | Run the command_line.py with the --help option to see a list of export options. Optionally, if you don't want to install python, there are builds for Mac and Windows in the command_line_builds folder of this repository.
94 |
95 | Example usage (if using the prebuilt binary, replace `python3.4 command_line.py` with the exe name):
96 |
97 | ```
98 | python3.4 command_line.py /var/www/html/CargoBlaster/ --main html/index.html --export-to linux-x64 windows mac --width 900 --height 700 --nw-version 0.10.5
99 | ```
100 |
101 | ### GUI
102 |
103 | Install dependencies:
104 |
105 | ```
106 | pip install -r requirements.txt
107 | ```
108 |
109 | Initiate submodules:
110 |
111 | ```
112 | git submodule update --init --recursive
113 | ```
114 |
115 | Run with:
116 |
117 | ```
118 | python3.4 main.py
119 | ```
120 |
121 | General Instructions for exporting:
122 |
123 | 1. Choose a project folder with at least one html or php file. The name of the export application will be autogenerated from the folder that you choose, so change it if you so desire.
124 | 2. Modify the options as needed.
125 | 3. Choose at least one export platform and then the Export button should be enabled (as long as the field names marked with a star are filled out and all files in the fields exist).
126 | 4. Click the export button and once it's done, click the "Open Export Folder" button to go to the folder where your exported project will be.
127 |
128 |
129 | ### Issues?
130 |
131 | If you have an issue, please check the FAQ before filing an issue to see if it helps.
132 |
133 | [FAQ](https://github.com/jyapayne/Web2Executable/wiki/FAQ)
134 |
135 |
136 | ### Additional Info
137 |
138 | [Changelog](https://github.com/jyapayne/Web2Executable/releases)
139 |
140 | [Screenshots](https://github.com/jyapayne/Web2Executable/wiki/Screenshots)
141 |
142 |
143 | License
144 | -------
145 |
146 | The MIT License (MIT)
147 |
148 | Copyright (c) 2015 SimplyPixelated
149 |
150 | Permission is hereby granted, free of charge, to any person obtaining a copy
151 | of this software and associated documentation files (the "Software"), to deal
152 | in the Software without restriction, including without limitation the rights
153 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
154 | copies of the Software, and to permit persons to whom the Software is
155 | furnished to do so, subject to the following conditions:
156 |
157 | The above copyright notice and this permission notice shall be included in all
158 | copies or substantial portions of the Software.
159 |
160 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
161 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
162 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
163 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
164 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
165 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
166 | SOFTWARE.
167 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | """
2 | Configuration module that holds some configuration options for
3 | Web2Executable.
4 | """
5 |
6 | from __future__ import print_function
7 | import os
8 | import logging
9 | import logging.handlers as lh
10 | import traceback
11 | import sys
12 | import zipfile
13 | import ssl
14 |
15 | import utils
16 |
17 | ZIP_MODE = zipfile.ZIP_STORED
18 |
19 | MAX_RECENT = 10
20 |
21 | DEBUG = False
22 | TESTING = False
23 |
24 | SSL_CONTEXT = ssl._create_unverified_context()
25 |
26 | ### The following sections are code that needs to be run when importing
27 | ### from main.py.
28 |
29 | ## CWD Computation --------------------------------------
30 |
31 | inside_packed_exe = getattr(sys, "frozen", "")
32 |
33 | if inside_packed_exe:
34 | # we are running in a |PyInstaller| bundle
35 | CWD = os.path.dirname(sys.executable)
36 | else:
37 | # we are running in a normal Python environment
38 | CWD = os.path.dirname(os.path.realpath(__file__))
39 |
40 | ## CMD Utility functions --------------------------------
41 |
42 |
43 | def get_file(path):
44 | parts = path.split("/")
45 | independent_path = utils.path_join(CWD, *parts)
46 | return independent_path
47 |
48 |
49 | def is_installed():
50 | uninst = get_file("uninst.exe")
51 | return utils.is_windows() and os.path.exists(uninst)
52 |
53 |
54 | ## Version Setting ----------------------------------------
55 |
56 | __version__ = "v0.0.0"
57 |
58 | with open(get_file("files/version.txt")) as f:
59 | __version__ = f.read().strip()
60 |
61 | ICON_PATH = "files/images/icon.png"
62 | WARNING_ICON = "files/images/warning.png"
63 | APP_SETTINGS_ICON = "files/images/app_settings.png"
64 | WINDOW_SETTINGS_ICON = "files/images/window_settings.png"
65 | EXPORT_SETTINGS_ICON = "files/images/export_settings.png"
66 | COMPRESS_SETTINGS_ICON = "files/images/compress_settings.png"
67 | DOWNLOAD_SETTINGS_ICON = "files/images/download_settings.png"
68 | FOLDER_OPEN_ICON = "files/images/folder_open.png"
69 |
70 | W2E_VER_FILE = "files/version.txt"
71 |
72 | TEMP_DIR = utils.get_temp_dir()
73 |
74 | ERROR_LOG_FILE = "files/error.log"
75 | VER_FILE = "files/nw-versions.txt"
76 | SETTINGS_FILE = "files/settings.cfg"
77 | GLOBAL_JSON_FILE = "files/global.json"
78 | WEB2EXE_JSON_FILE = "web2exe.json"
79 |
80 | LAST_PROJECT_FILE = "files/last_project_path.txt"
81 | RECENT_FILES_FILE = "files/recent_files.txt"
82 |
83 | NW_BRANCH_FILE = "files/nw-branch.txt"
84 |
85 | UPX_WIN_PATH = "files/compressors/upx-win.exe"
86 | UPX_MAC_PATH = "files/compressors/upx-mac"
87 | UPX_LIN32_PATH = "files/compressors/upx-linux-x32"
88 | UPX_LIN64_PATH = "files/compressors/upx-linux-x64"
89 |
90 | ENV_VARS_PY_PATH = "files/env_vars.py"
91 | ENV_VARS_BAT_PATH = "files/env_vars.bat"
92 | ENV_VARS_BASH_PATH = "files/env_vars.bash"
93 |
94 | ## Logger setup ----------------------------------------------
95 |
96 | LOG_FILENAME = utils.get_data_file_path(ERROR_LOG_FILE)
97 |
98 |
99 | def getLogHandler():
100 | return lh.RotatingFileHandler(
101 | LOG_FILENAME, maxBytes=100000, backupCount=2, encoding="utf-8"
102 | )
103 |
104 |
105 | if DEBUG:
106 | logging.basicConfig(
107 | format=(
108 | "%(levelname) -10s %(asctime)s %(module)s.py: "
109 | "%(lineno)s %(funcName)s - %(message)s"
110 | ),
111 | level=logging.DEBUG,
112 | handlers=[getLogHandler()],
113 | )
114 | else:
115 | logging.basicConfig(
116 | format=(
117 | "%(levelname) -10s %(asctime)s %(module)s.py: "
118 | "%(lineno)s %(funcName)s - %(message)s"
119 | ),
120 | level=logging.INFO,
121 | handlers=[getLogHandler()],
122 | )
123 |
124 |
125 | def getLogger(name):
126 | logger = logging.getLogger(name)
127 | logger.addHandler(getLogHandler())
128 | return logger
129 |
130 |
131 | logger = getLogger(__name__)
132 |
133 |
134 | ## Custom except hook to log all errors ----------------------
135 |
136 |
137 | def my_excepthook(type_, value, tback):
138 | output_err = "".join([x for x in traceback.format_exception(type_, value, tback)])
139 | logger.error("{}".format(output_err))
140 | sys.__excepthook__(type_, value, tback)
141 |
142 |
143 | sys.excepthook = my_excepthook
144 |
145 |
146 | def download_path(path=None):
147 | # Ensure that the default download path exists
148 | path = path or utils.get_data_path("files/downloads")
149 | try:
150 | os.makedirs(path)
151 | except:
152 | pass
153 | return path
154 |
--------------------------------------------------------------------------------
/files/base_url.txt:
--------------------------------------------------------------------------------
1 | http://dl.node-webkit.org/v{}/
2 |
--------------------------------------------------------------------------------
/files/compressors/upx-linux-x32:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/compressors/upx-linux-x32
--------------------------------------------------------------------------------
/files/compressors/upx-linux-x64:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/compressors/upx-linux-x64
--------------------------------------------------------------------------------
/files/compressors/upx-mac:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/compressors/upx-mac
--------------------------------------------------------------------------------
/files/compressors/upx-win.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/compressors/upx-win.exe
--------------------------------------------------------------------------------
/files/env_vars.bash:
--------------------------------------------------------------------------------
1 | PROJECT_DIR='{proj_dir}'
2 | PROJECT_NAME='{proj_name}'
3 | EXPORT_DIR='{export_dir}'
4 | EXPORT_DIRS=({export_dirs})
5 | MAC64_EXPORT_DIR='{mac-x64_dir}'
6 | MAC32_EXPORT_DIR='{mac-x32_dir}'
7 | WIN64_EXPORT_DIR='{windows-x64_dir}'
8 | WIN32_EXPORT_DIR='{windows-x32_dir}'
9 | LINUX64_EXPORT_DIR='{linux-x64_dir}'
10 | LINUX32_EXPORT_DIR='{linux-x32_dir}'
11 | NUM_DIRS='{num_dirs}'
12 |
--------------------------------------------------------------------------------
/files/env_vars.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | set "PROJECT_DIR={proj_dir}"
3 | set "PROJECT_NAME={proj_name}"
4 | set "EXPORT_DIR={export_dir}"
5 | set "MAC64_EXPORT_DIR={mac-x64_dir}"
6 | set "MAC32_EXPORT_DIR={mac-x32_dir}"
7 | set "WIN64_EXPORT_DIR={windows-x64_dir}"
8 | set "WIN32_EXPORT_DIR={windows-x32_dir}"
9 | set "LINUX64_EXPORT_DIR={linux-x64_dir}"
10 | set "LINUX32_EXPORT_DIR={linux-x32_dir}"
11 | set "NUM_DIRS={num_dirs}"
12 | {export_dirs}
13 |
--------------------------------------------------------------------------------
/files/env_vars.py:
--------------------------------------------------------------------------------
1 | PROJECT_DIR = "{proj_dir}"
2 | PROJECT_NAME = "{proj_name}"
3 | EXPORT_DIR = "{export_dir}"
4 | EXPORT_DIRS = {export_dirs}
5 | MAC64_EXPORT_DIR = "{mac-x64_dir}"
6 | MAC32_EXPORT_DIR = "{mac-x32_dir}"
7 | WIN64_EXPORT_DIR = "{windows-x64_dir}"
8 | WIN32_EXPORT_DIR = "{windows-x32_dir}"
9 | LINUX64_EXPORT_DIR = "{linux-x64_dir}"
10 | LINUX32_EXPORT_DIR = "{linux-x32_dir}"
11 | NUM_DIRS = {num_dirs}
12 |
--------------------------------------------------------------------------------
/files/images/app_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/app_settings.png
--------------------------------------------------------------------------------
/files/images/app_settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
127 |
--------------------------------------------------------------------------------
/files/images/compress_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/compress_settings.png
--------------------------------------------------------------------------------
/files/images/compress_settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
130 |
--------------------------------------------------------------------------------
/files/images/download_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/download_settings.png
--------------------------------------------------------------------------------
/files/images/download_settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
159 |
--------------------------------------------------------------------------------
/files/images/export_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/export_settings.png
--------------------------------------------------------------------------------
/files/images/export_settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
159 |
--------------------------------------------------------------------------------
/files/images/folder_open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/folder_open.png
--------------------------------------------------------------------------------
/files/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/icon.png
--------------------------------------------------------------------------------
/files/images/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/files/images/warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/warning.png
--------------------------------------------------------------------------------
/files/images/warning.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
85 |
--------------------------------------------------------------------------------
/files/images/window_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/window_settings.png
--------------------------------------------------------------------------------
/files/images/window_settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
148 |
--------------------------------------------------------------------------------
/files/nw-versions.txt:
--------------------------------------------------------------------------------
1 | 0.12.2
2 | 0.12.1
3 | 0.12.0
4 | 0.12.0-rc1
5 | 0.12.0-alpha3
6 | 0.12.0-alpha2
7 | 0.12.0-alpha1
8 | 0.11.3
9 | 0.11.2
10 | 0.11.1
11 | 0.11.0
12 | 0.11.0-rc1
13 | 0.10.5
14 | 0.10.4
15 | 0.10.3
16 | 0.10.2
17 | 0.10.1
18 | 0.10.0
19 | 0.10.0-rc2
20 | 0.10.0-rc1
21 | 0.9.2
22 | 0.9.1
23 | 0.9.0
24 | 0.8.6
25 | 0.8.5
26 | 0.8.4
27 | 0.8.3
28 | 0.8.2
29 | 0.8.1
30 | 0.8.0
31 | 0.7.2
32 | 0.7.1
33 | 0.7.0
34 | 0.6.3
35 | 0.6.2
36 | 0.6.1
37 | 0.6.0
38 |
--------------------------------------------------------------------------------
/files/settings.cfg:
--------------------------------------------------------------------------------
1 | base_url='https://dl.nwjs.io/v{}/'
2 | win_32_dir_prefix = 'nwjs-v{}-win-ia32'
3 | mac_32_dir_prefix = 'nwjs-v{}-osx-ia32'
4 | linux_32_dir_prefix = 'nwjs-v{}-linux-ia32'
5 |
6 | win_64_dir_prefix = 'nwjs-v{}-win-x64'
7 | mac_64_dir_prefix = 'nwjs-v{}-osx-x64'
8 | linux_64_dir_prefix = 'nwjs-v{}-linux-x64'
9 |
10 | [setting_groups]
11 | [[app_settings]]
12 | [[[main]]]
13 | display_name='Main html file'
14 | required=True
15 | type='file'
16 | file_types='*.html *.php *.htm'
17 | description='Main html file relative to the project directory.'
18 | [[[name]]]
19 | display_name='Name'
20 | required=True
21 | type='string'
22 | description='The name in the internal package.json. Must be alpha-numeric with no spaces.'
23 | filter='[a-z0-9_\-\.]+'
24 | filter_action='lower'
25 | [[[app_name]]]
26 | display_name='App Name'
27 | required=False
28 | type='string'
29 | description='The name that your executable or app will have when exported.'
30 | [[[description]]]
31 | default_value=None
32 | type='string'
33 | [[[version]]]
34 | default_value=None
35 | type='string'
36 | [[[keywords]]]
37 | default_value=None
38 | type='string'
39 | [[[nodejs]]]
40 | display_name='Include Nodejs'
41 | default_value=None
42 | type='check'
43 | [[[node-main]]]
44 | display_name='Node Main'
45 | default_value=None
46 | type='file'
47 | file_types='*.js'
48 | description='A path to a nodejs script file that will be executed on startup.'
49 | [[[domain]]]
50 | default_value=None
51 | type='string'
52 | description='Specify the host in the chrome-extension:// protocol URL used fo\n the application. The web engine will share the same cookies between your\napplication and the website under the same domain.'
53 | [[[user-agent]]]
54 | display_name='User Agent'
55 | default_value=None
56 | type='string'
57 | description='Overrides the User-Agent header in http requests.\n\nThe following placeholders are available to composite the user agent dynamically:\n\n%name: replaced by the name field in the manifest.\n%ver: replaced by the version field in the manifest, if available.\n%nwver: replaced by the version of NW.js.\n%webkit_ver: replaced by the version of WebKit engine.\n%osinfo: replaced by the OS and CPU information you would see in browser’s user agent string.'
58 | [[[node-remote]]]
59 | display_name='Node Remote'
60 | default_value=None
61 | type='strings'
62 | description='Enable calling node in remote pages. See the node-webkit\nmanifest format for more info.'
63 | [[[chromium-args]]]
64 | display_name='Chromium Args'
65 | default_value=None
66 | type='string'
67 | description='Specify chromium command line arguments.\nExample value: "--disable-accelerated-video --force-cpu-draw"'
68 | [[[js-flags]]]
69 | display_name='JS Flags'
70 | default_value=None
71 | type='string'
72 | description='Specify flags passed to the js engine.\nExample value: "--harmony_proxies --harmony_collecions"'
73 | [[[bg-script]]]
74 | display_name='Background Script'
75 | default_value=None
76 | type='file'
77 | file_types='*.js'
78 | description='Background script. The script is executed in the background page at\nthe start of application.'
79 | [[[inject_js_start]]]
80 | display_name='Inject JS Start'
81 | default_value=None
82 | type='file'
83 | file_types='*.js'
84 | description='A path to a js file that will be executed before any\nother script is run.'
85 | [[[inject_js_end]]]
86 | display_name='Inject JS End'
87 | default_value=None
88 | type='file'
89 | file_types='*.js'
90 | description='A path to a js file that will be executed after the\nDOM is loaded.'
91 | [[[additional_trust_anchors]]]
92 | display_name='Trust Anchors'
93 | default_value=None
94 | type='strings'
95 | description='A list of PEM-encoded certificates. Used as additional root\ncertificates for validation to allow connecting to services using a self-signed certificate.'
96 | [[[dom_storage_quota]]]
97 | display_name='DOM Storage (MB)'
98 | default_value=None
99 | type='int'
100 | description='Number of mega bytes (MB) for the quota of the DOM storage.\nThe suggestion is to put double the value you want.'
101 |
102 | [[webkit_settings]]
103 | [[[plugin]]]
104 | display_name='Load Plugins'
105 | default_value=None
106 | type='check'
107 | description='Whether to load external browser plugins like Flash.\nFor example, put the Pepper flash dll or so file in a directory at the root of\nyour project called "PepperFlash" and it will be loaded.'
108 | [[[double_tap_to_zoom_enabled]]]
109 | display_name='Double-Tap Zoom'
110 | default_value=None
111 | type='check'
112 | description='Enable zooming with double tapping on Mac OS X with 2 fingers. Mac OS X only'
113 |
114 | [[web2exe_settings]]
115 | [[[export_dir]]]
116 | display_name='Output Directory'
117 | default_value=''
118 | type='string'
119 | description='The output directory relative to the project directory.'
120 | [[[custom_script]]]
121 | display_name='Execute Script'
122 | default_value=''
123 | copy=False
124 | type='file'
125 | description='The script to execute after a project was successfully exported.'
126 | [[[output_pattern]]]
127 | display_name='Output Name Pattern'
128 | default_value=''
129 | type='string'
130 | description='Type "%(" to see a list of options to reference. Name your output folder.\n Include slashes to make sub-directories.'
131 | [[[blacklist]]]
132 | display_name='Blacklist'
133 | default_value=''
134 | type='string'
135 | description='Glob-style blacklist files/directories. Each line is a new pattern. Ex: *.jpeg, .git, *file[s].txt'
136 | [[[whitelist]]]
137 | display_name='Whitelist'
138 | default_value=''
139 | type='string'
140 | description='Glob-style whitelist files/directories. Each line is a new pattern. Ex: *.jpeg, .git, *file[s].txt.\nWhitelist trumps blacklist.'
141 |
142 | [[window_settings]]
143 | [[[id]]]
144 | default_value=None
145 | type='string'
146 | description='The id used to identify the window. This will be used to remember the\nsize and position of the window and restore that geometry when a\nwindow with the same id is later opened. '
147 | [[[title]]]
148 | default_value=None
149 | type='string'
150 | description='The default title of window created by NW.js, it’s very useful\nif you want to show your own title when the app is starting.'
151 | [[[icon]]]
152 | display_name='Window Icon'
153 | default_value=None
154 | type='file'
155 | action='set_window_icon'
156 | file_types='*.png *.jpg *.jpeg'
157 | [[[mac_icon]]]
158 | default_value=None
159 | type='file'
160 | action='set_mac_icon'
161 | file_types='*.png *.jpg *.jpeg *.icns'
162 | description='This icon to be displayed for the Mac Application.\nDefaults to Window Icon'
163 | [[[exe_icon]]]
164 | default_value=None
165 | type='file'
166 | action='set_exe_icon'
167 | file_types='*.png *.jpg *.jpeg'
168 | description='This icon to be displayed for the windows exe of the app.\nDefaults to Window icon.'
169 | [[[width]]]
170 | default_value=None
171 | type='int'
172 | [[[height]]]
173 | default_value=None
174 | type='int'
175 | [[[min_width]]]
176 | default_value=None
177 | type='int'
178 | [[[min_height]]]
179 | default_value=None
180 | type='int'
181 | [[[max_width]]]
182 | default_value=None
183 | type='int'
184 | [[[max_height]]]
185 | default_value=None
186 | type='int'
187 | [[[always_on_top]]]
188 | display_name='Keep on Top'
189 | default_value=None
190 | type='check'
191 | description='Makes the window always on top of other windows.'
192 | [[[frame]]]
193 | display_name='Window Frame'
194 | default_value=True
195 | type='check'
196 | description='Show the frame of the window'
197 | [[[show_in_taskbar]]]
198 | display_name='Taskbar'
199 | default_value=True
200 | type='check'
201 | description='Show the app running in the taskbar'
202 | [[[show]]]
203 | display_name='Show'
204 | default_value=True
205 | type='check'
206 | description='Uncheck to make your app hidden on startup.'
207 | [[[visible_on_all_workspaces]]]
208 | display_name='All Workspaces'
209 | default_value=None
210 | type='check'
211 | description='If checked, the exported app will be visible on all workspaces.\nMac & Linux Only'
212 | [[[visible]]]
213 | default_value=True
214 | type='check'
215 | description='If unchecked, the app will have to be manually set to\nvisible in javascript.'
216 | [[[resizable]]]
217 | default_value=True
218 | type='check'
219 | description=''
220 | [[[fullscreen]]]
221 | default_value=None
222 | type='check'
223 | description=''
224 | [[[position]]]
225 | display_name='Position by'
226 | default_value=None
227 | values=[None, 'mouse', 'center']
228 | type='list'
229 | description='The position to place the window when it opens.'
230 | [[[as_desktop]]]
231 | default_value=None
232 | type='check'
233 | description='Show as desktop background window under X11 environment. Linux Only.'
234 | [[[transparent]]]
235 | default_value=None
236 | type='check'
237 | description='Allows window tranparency.'
238 | [[[kiosk]]]
239 | default_value=None
240 | type='check'
241 | description='Puts the application is kiosk mode.'
242 | [[[kiosk_emulation]]]
243 | default_value=None
244 | type='check'
245 | description='Puts the application is kiosk emulation mode. Will\nautomatically check off required settings that will emulate kiosk.'
246 | check_action='set_kiosk_emulation_options'
247 |
248 | [[download_settings]]
249 | [[[nw_version]]]
250 | display_name='NW.js version'
251 | required=False
252 | default_value=None
253 | values=[]
254 | type='list'
255 | button='Update'
256 | button_callback='update_nw_versions'
257 | action='refresh_export'
258 | [[[sdk_build]]]
259 | display_name='SDK build'
260 | default_value=False
261 | type='check'
262 | description='Downloads the SDK version of NW.js to support devtools by\npressing F12 or ⌘+⌥+i.'
263 | [[[force_download]]]
264 | default_value=False
265 | type='check'
266 | [[[download_dir]]]
267 | display_name='Download location'
268 | default_value=''
269 | type='folder'
270 |
271 | [export_settings]
272 | [[windows-x32]]
273 | default_value=None
274 | type='check'
275 | url='%(base_url)s%(win_32_dir_prefix)s.zip'
276 | binary_location='nw.exe'
277 | system='windows'
278 | short_system='win'
279 | arch='x32'
280 | [[windows-x64]]
281 | default_value=None
282 | type='check'
283 | url='%(base_url)s%(win_64_dir_prefix)s.zip'
284 | binary_location='nw.exe'
285 | system='windows'
286 | short_system='win'
287 | arch='x64'
288 | [[mac-x64]]
289 | default_value=None
290 | type='check'
291 | url='%(base_url)s%(mac_64_dir_prefix)s.zip'
292 | system='mac'
293 | short_system='mac'
294 | arch='x64'
295 | [[linux-x64]]
296 | default_value=None
297 | type='check'
298 | url='%(base_url)s%(linux_64_dir_prefix)s.tar.gz'
299 | binary_location='nw'
300 | system='linux'
301 | short_system='lin'
302 | arch='x64'
303 | [[linux-x32]]
304 | default_value=None
305 | type='check'
306 | url='%(base_url)s%(linux_32_dir_prefix)s.tar.gz'
307 | binary_location='nw'
308 | system='linux'
309 | short_system='lin'
310 | arch='x32'
311 |
312 | [compression]
313 | [[nw_compression_level]]
314 | display_name='Compression Level'
315 | default_value=0
316 | min=0
317 | max=9
318 | type='range'
319 | description='Compression to be applied to the executable\'s nwjs binary.\n0 is no compression, 9 is maximum. They all use lzma.'
320 | [[uncompressed_folder]]
321 | display_name='Uncompressed Folder'
322 | type='check'
323 | default_value=False
324 | description='This option makes the resulting app.nw inside the app just a\nplain folder. This is useful to mitigate startup\ntimes and to modify files.'
325 |
326 |
327 | [order]
328 | application_setting_order="""['main', 'name', 'app_name', 'node-main', 'description', 'version', 'keywords',
329 | 'user-agent', 'chromium-args', 'domain',
330 | 'node-remote', 'js-flags', 'bg-script', 'inject_js_start', 'inject_js_end',
331 | 'additional_trust_anchors',
332 | 'nodejs', 'plugin', 'double_tap_to_zoom_enabled']"""
333 | window_setting_order = """['id','title', 'icon', 'mac_icon', 'exe_icon', 'position', 'width', 'height',
334 | 'min_width', 'min_height',
335 | 'max_width', 'max_height', 'dom_storage_quota', 'always_on_top', 'frame',
336 | 'show_in_taskbar', 'show', 'visible', 'visible_on_all_workspaces',
337 | 'resizable', 'fullscreen', 'as_desktop',
338 | 'kiosk', 'kiosk_emulation', 'transparent']"""
339 |
340 | export_setting_order = """['windows-x32', 'windows-x64', 'mac-x64', 'linux-x64', 'linux-x32']"""
341 | compression_setting_order = """['nw_compression_level', 'uncompressed_folder']"""
342 |
343 | download_setting_order = """['nw_version', 'sdk_build', 'download_dir',
344 | 'force_download']"""
345 |
346 | [version_info]
347 | urls="""[('https://raw.githubusercontent.com/nwjs/nw.js/{}/CHANGELOG.md', '(\S+) / \d{2}-\d{2}-\d{4}'), ('http://nwjs.io/blog/', 'NW.js v(\S+) ')]"""
348 | github_api_url="https://api.github.com/repos/nwjs/nw.js"
349 |
--------------------------------------------------------------------------------
/files/version.txt:
--------------------------------------------------------------------------------
1 | v0.7.2b
2 |
--------------------------------------------------------------------------------
/image_utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/image_utils/__init__.py
--------------------------------------------------------------------------------
/image_utils/image_utils.py:
--------------------------------------------------------------------------------
1 | from io import BytesIO
2 |
3 | try:
4 | from PIL import Image as im
5 |
6 | IMAGE_UTILS_AVAILABLE = True
7 | Image = im.open
8 |
9 | def resize(image, size):
10 | output = BytesIO()
11 | back = im.new("RGBA", size, (0, 0, 0, 0))
12 | image.thumbnail(size, im.ANTIALIAS)
13 | offset = [0, 0]
14 | if image.size[0] >= image.size[1]:
15 | offset[1] = int(back.size[1] / 2 - image.size[1] / 2)
16 | else:
17 | offset[0] = int(back.size[0] / 2 - image.size[0] / 2)
18 | back.paste(image, tuple(offset))
19 | back.save(output, image.format)
20 | contents = output.getvalue()
21 | output.close()
22 | return contents
23 |
24 | except ImportError:
25 | IMAGE_UTILS_AVAILABLE = False
26 |
27 | LARGEST_ICON_SIZE = 1024
28 | SMALLEST_ICON_SIZE = 16
29 | sizes = [LARGEST_ICON_SIZE, 512, 256, 128, 48, 32, SMALLEST_ICON_SIZE]
30 |
31 |
32 | def nearest_icon_size(width, height):
33 | maximum = max(width, height)
34 |
35 | if maximum >= LARGEST_ICON_SIZE:
36 | return LARGEST_ICON_SIZE
37 | if maximum <= SMALLEST_ICON_SIZE:
38 | return SMALLEST_ICON_SIZE
39 |
40 | for i in range(len(sizes) - 1):
41 | current_size = sizes[i]
42 | next_size = sizes[i + 1]
43 | if current_size > maximum >= next_size:
44 | return next_size
45 |
--------------------------------------------------------------------------------
/image_utils/pycns.py:
--------------------------------------------------------------------------------
1 | from image_utils.icns_info import ICNSHeader, icns_to_png
2 | from image_utils.image_utils import Image
3 | import sys
4 |
5 | """This module takes any image that is readable by PIL and exports it to an icns file.
6 | The image will be scaled keeping the aspect ratio if it is a non square image.
7 | """
8 |
9 |
10 | def encode_image_to_icns(image_path):
11 | """Takes an image and converts it to icns. Image aspect ratio will remain intact."""
12 | i = Image(image_path)
13 | icns_header = ICNSHeader()
14 | f_data = icns_header.parse_image(i)
15 | return f_data
16 |
17 |
18 | def save_icns(image_path, icns_path):
19 | im_data = encode_image_to_icns(image_path)
20 | if im_data is not None:
21 | icns_path = icns_path if icns_path.endswith(".icns") else icns_path + ".icns"
22 | open(icns_path, "wb+").write(im_data)
23 |
24 |
25 | def pngs_from_icns(icns_path):
26 | return icns_to_png(icns_path)
27 |
28 |
29 | if __name__ == "__main__":
30 | if len(sys.argv) != 3:
31 | print("Usage: pycns input_image_path output_icns_path")
32 | sys.exit()
33 |
34 | image_path, icns_path = sys.argv[1:3]
35 |
36 | save_icns(image_path, icns_path)
37 |
--------------------------------------------------------------------------------
/images/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/images/icon.icns
--------------------------------------------------------------------------------
/images/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/images/icon.ico
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/images/icon.png
--------------------------------------------------------------------------------
/pe.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright Joey Payne All Rights Reserved
3 | """
4 |
5 | import os
6 | import struct
7 | from io import BytesIO
8 |
9 | from PIL import Image
10 |
11 |
12 | def resize(image, size, format=None):
13 | output = BytesIO()
14 | back = Image.new("RGBA", size, (0, 0, 0, 0))
15 |
16 | if image.size[0] < size[0] or image.size[1] < size[1]:
17 | if image.height > image.width:
18 | factor = size[0] / image.height
19 | else:
20 | factor = size[1] / image.width
21 | image = image.resize(
22 | (int(image.width * factor), int(image.height * factor)), Image.ANTIALIAS
23 | )
24 | else:
25 | image.thumbnail(size, Image.ANTIALIAS)
26 |
27 | offset = [0, 0]
28 | if image.size[0] > image.size[1]:
29 | offset[1] = int(back.size[1] / 2 - image.size[1] / 2)
30 | elif image.size[0] < image.size[1]:
31 | offset[0] = int(back.size[0] / 2 - image.size[0] / 2)
32 | else:
33 | offset[0] = int(back.size[0] / 2 - image.size[0] / 2)
34 | offset[1] = int(back.size[1] / 2 - image.size[1] / 2)
35 |
36 | back.paste(image, tuple(offset))
37 | format = format or image.format
38 | back.save(output, format, sizes=[size])
39 | contents = output.getvalue()
40 | output.close()
41 | return contents
42 |
43 |
44 | struct_symbols = {
45 | 1: "B", # byte
46 | 2: "H", # word
47 | 4: "L", # long word
48 | 8: "Q", # double long word
49 | }
50 | endian_symbols = {"little": "<", "big": ">"}
51 |
52 | name_dictionary = {
53 | "PEHeader_Machine": {
54 | 0: "IMAGE_FILE_MACHINE_UNKNOWN",
55 | 0x014C: "IMAGE_FILE_MACHINE_I386",
56 | 0x0162: "IMAGE_FILE_MACHINE_R3000",
57 | 0x0166: "IMAGE_FILE_MACHINE_R4000",
58 | 0x0168: "IMAGE_FILE_MACHINE_R10000",
59 | 0x0169: "IMAGE_FILE_MACHINE_WCEMIPSV2",
60 | 0x0184: "IMAGE_FILE_MACHINE_ALPHA",
61 | 0x01A2: "IMAGE_FILE_MACHINE_SH3",
62 | 0x01A3: "IMAGE_FILE_MACHINE_SH3DSP",
63 | 0x01A4: "IMAGE_FILE_MACHINE_SH3E",
64 | 0x01A6: "IMAGE_FILE_MACHINE_SH4",
65 | 0x01A8: "IMAGE_FILE_MACHINE_SH5",
66 | 0x01C0: "IMAGE_FILE_MACHINE_ARM",
67 | 0x01C2: "IMAGE_FILE_MACHINE_THUMB",
68 | 0x01C4: "IMAGE_FILE_MACHINE_ARMNT",
69 | 0x01D3: "IMAGE_FILE_MACHINE_AM33",
70 | 0x01F0: "IMAGE_FILE_MACHINE_POWERPC",
71 | 0x01F1: "IMAGE_FILE_MACHINE_POWERPCFP",
72 | 0x0200: "IMAGE_FILE_MACHINE_IA64",
73 | 0x0266: "IMAGE_FILE_MACHINE_MIPS16",
74 | 0x0284: "IMAGE_FILE_MACHINE_ALPHA64",
75 | 0x0284: "IMAGE_FILE_MACHINE_AXP64", # same
76 | 0x0366: "IMAGE_FILE_MACHINE_MIPSFPU",
77 | 0x0466: "IMAGE_FILE_MACHINE_MIPSFPU16",
78 | 0x0520: "IMAGE_FILE_MACHINE_TRICORE",
79 | 0x0CEF: "IMAGE_FILE_MACHINE_CEF",
80 | 0x0EBC: "IMAGE_FILE_MACHINE_EBC",
81 | 0x8664: "IMAGE_FILE_MACHINE_AMD64",
82 | 0x9041: "IMAGE_FILE_MACHINE_M32R",
83 | 0xC0EE: "IMAGE_FILE_MACHINE_CEE",
84 | },
85 | "PEHeader_Characteristics": {
86 | 0x0001: "IMAGE_FILE_RELOCS_STRIPPED",
87 | 0x0002: "IMAGE_FILE_EXECUTABLE_IMAGE",
88 | 0x0004: "IMAGE_FILE_LINE_NUMS_STRIPPED",
89 | 0x0008: "IMAGE_FILE_LOCAL_SYMS_STRIPPED",
90 | 0x0010: "IMAGE_FILE_AGGRESIVE_WS_TRIM",
91 | 0x0020: "IMAGE_FILE_LARGE_ADDRESS_AWARE",
92 | 0x0040: "IMAGE_FILE_16BIT_MACHINE",
93 | 0x0080: "IMAGE_FILE_BYTES_REVERSED_LO",
94 | 0x0100: "IMAGE_FILE_32BIT_MACHINE",
95 | 0x0200: "IMAGE_FILE_DEBUG_STRIPPED",
96 | 0x0400: "IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP",
97 | 0x0800: "IMAGE_FILE_NET_RUN_FROM_SWAP",
98 | 0x1000: "IMAGE_FILE_SYSTEM",
99 | 0x2000: "IMAGE_FILE_DLL",
100 | 0x4000: "IMAGE_FILE_UP_SYSTEM_ONLY",
101 | 0x8000: "IMAGE_FILE_BYTES_REVERSED_HI",
102 | },
103 | "OptionalHeader_Subsystem": {
104 | 0: "IMAGE_SUBSYSTEM_UNKNOWN",
105 | 1: "IMAGE_SUBSYSTEM_NATIVE",
106 | 2: "IMAGE_SUBSYSTEM_WINDOWS_GUI",
107 | 3: "IMAGE_SUBSYSTEM_WINDOWS_CUI",
108 | 5: "IMAGE_SUBSYSTEM_OS2_CUI",
109 | 7: "IMAGE_SUBSYSTEM_POSIX_CUI",
110 | 8: "IMAGE_SUBSYSTEM_NATIVE_WINDOWS",
111 | 9: "IMAGE_SUBSYSTEM_WINDOWS_CE_GUI",
112 | 10: "IMAGE_SUBSYSTEM_EFI_APPLICATION",
113 | 11: "IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER",
114 | 12: "IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER",
115 | 13: "IMAGE_SUBSYSTEM_EFI_ROM",
116 | 14: "IMAGE_SUBSYSTEM_XBOX",
117 | 16: "IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION",
118 | },
119 | "OptionalHeader_DLL_Characteristics": {
120 | 0x0001: "IMAGE_LIBRARY_PROCESS_INIT", # reserved
121 | 0x0002: "IMAGE_LIBRARY_PROCESS_TERM", # reserved
122 | 0x0004: "IMAGE_LIBRARY_THREAD_INIT", # reserved
123 | 0x0008: "IMAGE_LIBRARY_THREAD_TERM", # reserved
124 | 0x0020: "IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA",
125 | 0x0040: "IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE",
126 | 0x0080: "IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY",
127 | 0x0100: "IMAGE_DLLCHARACTERISTICS_NX_COMPAT",
128 | 0x0200: "IMAGE_DLLCHARACTERISTICS_NO_ISOLATION",
129 | 0x0400: "IMAGE_DLLCHARACTERISTICS_NO_SEH",
130 | 0x0800: "IMAGE_DLLCHARACTERISTICS_NO_BIND",
131 | 0x1000: "IMAGE_DLLCHARACTERISTICS_APPCONTAINER",
132 | 0x2000: "IMAGE_DLLCHARACTERISTICS_WDM_DRIVER",
133 | 0x4000: "IMAGE_DLLCHARACTERISTICS_GUARD_CF",
134 | 0x8000: "IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE",
135 | },
136 | "SectionHeader_Characteristics": {
137 | 0x00000000: "IMAGE_SCN_TYPE_REG", # reserved
138 | 0x00000001: "IMAGE_SCN_TYPE_DSECT", # reserved
139 | 0x00000002: "IMAGE_SCN_TYPE_NOLOAD", # reserved
140 | 0x00000004: "IMAGE_SCN_TYPE_GROUP", # reserved
141 | 0x00000008: "IMAGE_SCN_TYPE_NO_PAD", # reserved
142 | 0x00000010: "IMAGE_SCN_TYPE_COPY", # reserved
143 | 0x00000020: "IMAGE_SCN_CNT_CODE",
144 | 0x00000040: "IMAGE_SCN_CNT_INITIALIZED_DATA",
145 | 0x00000080: "IMAGE_SCN_CNT_UNINITIALIZED_DATA",
146 | 0x00000100: "IMAGE_SCN_LNK_OTHER",
147 | 0x00000200: "IMAGE_SCN_LNK_INFO",
148 | 0x00000400: "IMAGE_SCN_LNK_OVER", # reserved
149 | 0x00000800: "IMAGE_SCN_LNK_REMOVE",
150 | 0x00001000: "IMAGE_SCN_LNK_COMDAT",
151 | 0x00004000: "IMAGE_SCN_MEM_PROTECTED", # obsolete
152 | 0x00004000: "IMAGE_SCN_NO_DEFER_SPEC_EXC",
153 | 0x00008000: "IMAGE_SCN_GPREL",
154 | 0x00008000: "IMAGE_SCN_MEM_FARDATA",
155 | 0x00010000: "IMAGE_SCN_MEM_SYSHEAP", # obsolete
156 | 0x00020000: "IMAGE_SCN_MEM_PURGEABLE",
157 | 0x00020000: "IMAGE_SCN_MEM_16BIT",
158 | 0x00040000: "IMAGE_SCN_MEM_LOCKED",
159 | 0x00080000: "IMAGE_SCN_MEM_PRELOAD",
160 | 0x00100000: "IMAGE_SCN_ALIGN_1BYTES",
161 | 0x00200000: "IMAGE_SCN_ALIGN_2BYTES",
162 | 0x00300000: "IMAGE_SCN_ALIGN_4BYTES",
163 | 0x00400000: "IMAGE_SCN_ALIGN_8BYTES",
164 | 0x00500000: "IMAGE_SCN_ALIGN_16BYTES", # default alignment
165 | 0x00600000: "IMAGE_SCN_ALIGN_32BYTES",
166 | 0x00700000: "IMAGE_SCN_ALIGN_64BYTES",
167 | 0x00800000: "IMAGE_SCN_ALIGN_128BYTES",
168 | 0x00900000: "IMAGE_SCN_ALIGN_256BYTES",
169 | 0x00A00000: "IMAGE_SCN_ALIGN_512BYTES",
170 | 0x00B00000: "IMAGE_SCN_ALIGN_1024BYTES",
171 | 0x00C00000: "IMAGE_SCN_ALIGN_2048BYTES",
172 | 0x00D00000: "IMAGE_SCN_ALIGN_4096BYTES",
173 | 0x00E00000: "IMAGE_SCN_ALIGN_8192BYTES",
174 | 0x00F00000: "IMAGE_SCN_ALIGN_MASK",
175 | 0x01000000: "IMAGE_SCN_LNK_NRELOC_OVFL",
176 | 0x02000000: "IMAGE_SCN_MEM_DISCARDABLE",
177 | 0x04000000: "IMAGE_SCN_MEM_NOT_CACHED",
178 | 0x08000000: "IMAGE_SCN_MEM_NOT_PAGED",
179 | 0x10000000: "IMAGE_SCN_MEM_SHARED",
180 | 0x20000000: "IMAGE_SCN_MEM_EXECUTE",
181 | 0x40000000: "IMAGE_SCN_MEM_READ",
182 | 0x80000000: "IMAGE_SCN_MEM_WRITE",
183 | },
184 | }
185 |
186 | DEFAULT_ENDIAN = "little"
187 |
188 |
189 | def read_data(file_data, offset, number_of_bytes, string_data=None):
190 | """Just reads the straight data with no endianness."""
191 |
192 | if number_of_bytes > 0:
193 | data = file_data[offset : offset + number_of_bytes]
194 | # if len(data) != number_of_bytes:
195 | # print 'data out of bounds:', 'offset', hex(offset), 'data', data, 'data_len', len(data), 'num_bytes', number_of_bytes, 'total', hex(len(file_data))
196 | return data
197 | else:
198 | return bytearray("")
199 |
200 |
201 | def read_bytes(file_data, offset, number_of_bytes, endian=None, string_data=None):
202 | """Returns a tuple of the data value and string representation.
203 | Will read 1,2,4,8 bytes with little endian as the default
204 | (value, string)
205 | """
206 |
207 | if number_of_bytes > 0:
208 | endian = endian or DEFAULT_ENDIAN
209 | endian = endian_symbols[endian]
210 |
211 | data = bytes(file_data[offset : offset + number_of_bytes])
212 | if len(data) != number_of_bytes:
213 | return 0, ""
214 |
215 | return struct.unpack(endian + struct_symbols[number_of_bytes], data)[0], data
216 | else:
217 | return 0, ""
218 |
219 |
220 | def value_to_byte_string(value, number_of_bytes, endian=None):
221 | endian = endian or DEFAULT_ENDIAN
222 | endian = endian_symbols[endian]
223 |
224 | return struct.pack(endian + struct_symbols[number_of_bytes], value)
225 |
226 |
227 | class ResourceTypes(object):
228 | Cursor = 1
229 | Bitmap = 2
230 | Icon = 3
231 | Menu = 4
232 | Dialog = 5
233 | String = 6
234 | Font_Directory = 7
235 | Font = 8
236 | Accelerator = 9
237 | RC_Data = 10
238 | Message_Table = 11
239 | Group_Cursor = 12
240 | Group_Icon = 14
241 | Version_Info = 16
242 | DLG_Include = 17
243 | Plug_Play = 19
244 | VXD = 20
245 | Animated_Cursor = 21
246 | Animated_Icon = 22
247 | HTML = 23
248 | Manifest = 24
249 |
250 |
251 | resource_types = {
252 | 1: "Cursor",
253 | 2: "Bitmap",
254 | 3: "Icon",
255 | 4: "Menu",
256 | 5: "Dialog",
257 | 6: "String",
258 | 7: "Font Directory",
259 | 8: "Font",
260 | 9: "Accelerator",
261 | 10: "RC Data",
262 | 11: "Message Table",
263 | 12: "Group Cursor",
264 | 14: "Group Icon",
265 | 16: "Version Info",
266 | 17: "DLG Include",
267 | 19: "Plug and Play",
268 | 20: "VXD",
269 | 21: "Animated Cursor",
270 | 22: "Animated Icon",
271 | 23: "HTML",
272 | 24: "Manifest",
273 | }
274 |
275 | _32BIT_PLUS_MAGIC = 0x20B
276 | _32BIT_MAGIC = 0x10B
277 | _ROM_MAGIC = 0x107
278 |
279 |
280 | def read_from_name_dict(obj, field_name):
281 | dict_field = "{}_{}".format(obj.__class__.__name__, field_name)
282 | return name_dictionary.get(dict_field, {})
283 |
284 |
285 | def test_bit(value, index):
286 | mask = 1 << index
287 | return value & mask
288 |
289 |
290 | def set_bit(value, index):
291 | mask = 1 << index
292 | return value | mask
293 |
294 |
295 | def clear_bit(value, index):
296 | mask = ~(1 << index)
297 | return value & mask
298 |
299 |
300 | def toggle_bit(value, index):
301 | mask = 1 << index
302 | return value ^ mask
303 |
304 |
305 | class PEFormatError(Exception):
306 | pass
307 |
308 |
309 | class Printable(object):
310 | def _attrs(self):
311 | a = []
312 | for attr in dir(self):
313 | if not attr.startswith("_") and not callable(getattr(self, attr)):
314 | a.append(attr)
315 | return a
316 |
317 | def _dict_items(self):
318 | for a in reversed(self._attrs()):
319 | yield a, getattr(self, a)
320 |
321 | def _dict_string(self):
322 | vals = []
323 | for key, val in self._dict_items():
324 | try:
325 | vals.append("{}={}".format(key, val))
326 | except UnicodeDecodeError:
327 | vals.append("{}=".format(key))
328 | return ", ".join(vals)
329 |
330 | def __repr__(self):
331 | return str(self)
332 |
333 | def __str__(self):
334 | return "{} [{}]".format(self.__class__.__name__, self._dict_string())
335 |
336 |
337 | class Structure(Printable):
338 | _fields = {}
339 |
340 | def __init__(
341 | self,
342 | size=0,
343 | value=None,
344 | data=None,
345 | absolute_offset=0,
346 | name="",
347 | friendly_name="",
348 | *args,
349 | **kwargs
350 | ):
351 | self._value = value
352 | self.size = size
353 | self.data = data
354 | self.name = name
355 | self.friendly_name = friendly_name
356 | self._absolute_offset = absolute_offset
357 | self._file_data = None
358 |
359 | for k, v in kwargs.items():
360 | setattr(self, k, v)
361 |
362 | @property
363 | def absolute_offset(self):
364 | return self._absolute_offset
365 |
366 | @absolute_offset.setter
367 | def absolute_offset(self, abs_offset):
368 | self._absolute_offset = abs_offset
369 | for k, v in self._fields.items():
370 | field = getattr(self, k)
371 | field.absolute_offset = self.absolute_offset + field.offset
372 |
373 | @property
374 | def value(self):
375 | return self._value
376 |
377 | @value.setter
378 | def value(self, value):
379 | if self._file_data is not None:
380 | self.data = value_to_byte_string(value, self.size)
381 | self._file_data[
382 | self.absolute_offset : self.absolute_offset + self.size
383 | ] = bytearray(self.data)
384 | self._value = value
385 |
386 | def process_field(self, file_data, field_name, field_info):
387 | if hasattr(self, "process_" + field_name) and callable(
388 | getattr(self, "process_" + field_name)
389 | ):
390 | getattr(self, "process_" + field_name)(file_data, field_name, field_info)
391 | else:
392 | absolute_offset = field_info["offset"] + self.absolute_offset
393 | size = field_info["size"]
394 | self.size += size
395 | int_value, data = read_bytes(file_data, absolute_offset, size)
396 | field_name_dict = read_from_name_dict(self, field_name)
397 | name = field_name_dict.get(int_value, "")
398 | friendly_name = name.replace("_", " ").capitalize()
399 |
400 | setattr(
401 | self,
402 | field_name,
403 | Structure(
404 | offset=field_info["offset"],
405 | size=size,
406 | value=int_value,
407 | data=data,
408 | absolute_offset=absolute_offset,
409 | name=name,
410 | friendly_name=friendly_name,
411 | ),
412 | )
413 | getattr(self, field_name)._file_data = file_data
414 |
415 | def process_Characteristics(self, file_data, field_name, field_info):
416 | absolute_offset = field_info["offset"] + self.absolute_offset
417 | size = field_info["size"]
418 | self.size += size
419 | int_value, data = read_bytes(file_data, absolute_offset, size)
420 | field_name_dict = read_from_name_dict(self, field_name)
421 |
422 | bit_length = len(bin(int_value)) - 2
423 |
424 | characteristics = {}
425 | for i in range(bit_length):
426 | set_bit = test_bit(int_value, i)
427 | char_name = field_name_dict.get(set_bit, "")
428 | if set_bit != 0 and char_name:
429 | characteristics[char_name] = set_bit
430 |
431 | setattr(
432 | self,
433 | field_name,
434 | Structure(
435 | offset=field_info["offset"],
436 | size=size,
437 | value=int_value,
438 | data=data,
439 | absolute_offset=absolute_offset,
440 | values=characteristics,
441 | ),
442 | )
443 | getattr(self, field_name)._file_data = file_data
444 |
445 | @classmethod
446 | def parse_from_data(cls, file_data, **cls_args):
447 | """Parses the Structure from the file data."""
448 | self = cls(**cls_args)
449 | self._file_data = file_data
450 | for field_name, field_info in self._fields.items():
451 | self.process_field(file_data, field_name, field_info)
452 | return self
453 |
454 |
455 | class DOSHeader(Structure):
456 | """The dos header of the PE file"""
457 |
458 | _fields = {
459 | "Signature": {"offset": 0, "size": 2},
460 | "PEHeaderOffset": {"offset": 0x3C, "size": 4},
461 | }
462 |
463 |
464 | class PEHeader(Structure):
465 | """PE signature plus the COFF header"""
466 |
467 | _fields = {
468 | "Signature": {"offset": 0, "size": 4},
469 | "Machine": {"offset": 4, "size": 2},
470 | "NumberOfSections": {"offset": 6, "size": 2},
471 | "TimeDateStamp": {"offset": 8, "size": 4},
472 | "PointerToSymbolTable": {"offset": 12, "size": 4},
473 | "NumberOfSymbols": {"offset": 16, "size": 4},
474 | "SizeOfOptionalHeader": {"offset": 20, "size": 2},
475 | "Characteristics": {"offset": 22, "size": 2},
476 | }
477 |
478 |
479 | class OptionalHeader(Structure):
480 | _fields_32_plus = {
481 | "Magic": {"offset": 0, "size": 2},
482 | "MajorLinkerVersion": {"offset": 2, "size": 1},
483 | "MinorLinkerVersion": {"offset": 3, "size": 1},
484 | "SizeOfCode": {"offset": 4, "size": 4},
485 | "SizeOfInitializedData": {"offset": 8, "size": 4},
486 | "SizeOfUninitializedData": {"offset": 12, "size": 4},
487 | "AddressOfEntryPoint": {"offset": 16, "size": 4},
488 | "BaseOfCode": {"offset": 20, "size": 4},
489 | "ImageBase": {"offset": 24, "size": 8},
490 | "SectionAlignment": {"offset": 32, "size": 4},
491 | "FileAlignment": {"offset": 36, "size": 4},
492 | "MajorOperatingSystemVersion": {"offset": 40, "size": 2},
493 | "MinorOperatingSystemVersion": {"offset": 42, "size": 2},
494 | "MajorImageVersion": {"offset": 44, "size": 2},
495 | "MinorImageVersion": {"offset": 46, "size": 2},
496 | "MajorSubsystemVersion": {"offset": 48, "size": 2},
497 | "MinorSubsystemVersion": {"offset": 50, "size": 2},
498 | "Reserved": {"offset": 52, "size": 4},
499 | "SizeOfImage": {"offset": 56, "size": 4},
500 | "SizeOfHeaders": {"offset": 60, "size": 4},
501 | "SizeOfHeaders": {"offset": 60, "size": 4},
502 | "CheckSum": {"offset": 64, "size": 4},
503 | "Subsystem": {"offset": 68, "size": 2},
504 | "DLL_Characteristics": {"offset": 70, "size": 2},
505 | "SizeOfStackReserve": {"offset": 72, "size": 8},
506 | "SizeOfStackCommit": {"offset": 80, "size": 8},
507 | "SizeOfHeapReserve": {"offset": 88, "size": 8},
508 | "SizeOfHeapCommit": {"offset": 96, "size": 8},
509 | "LoaderFlags": {"offset": 104, "size": 4},
510 | "NumberOfRvaAndSizes": {"offset": 108, "size": 4},
511 | "ExportTableAddress": {"offset": 112, "size": 4},
512 | "ExportTableSize": {"offset": 116, "size": 4},
513 | "ImportTableAddress": {"offset": 120, "size": 4},
514 | "ImportTableSize": {"offset": 124, "size": 4},
515 | "ResourceTableAddress": {"offset": 128, "size": 4},
516 | "ResourceTableSize": {"offset": 132, "size": 4},
517 | "ExceptionTableAddress": {"offset": 136, "size": 4},
518 | "ExceptionTableSize": {"offset": 140, "size": 4},
519 | "CertificateTableAddress": {"offset": 144, "size": 4},
520 | "CertificateTableSize": {"offset": 148, "size": 4},
521 | "BaseRelocationTableAddress": {"offset": 152, "size": 4},
522 | "BaseRelocationTableSize": {"offset": 156, "size": 4},
523 | "DebugAddress": {"offset": 160, "size": 4},
524 | "DebugSize": {"offset": 164, "size": 4},
525 | "ArchitectureAddress": {"offset": 168, "size": 4},
526 | "ArchitectureSize": {"offset": 172, "size": 4},
527 | "GlobalPtrAddress": {"offset": 176, "size": 8},
528 | "GlobalPtrSize": {"offset": 184, "size": 0},
529 | "ThreadLocalStorageTableAddress": {"offset": 184, "size": 4},
530 | "ThreadLocalStorageTableSize": {"offset": 188, "size": 4},
531 | "LoadConfigTableAddress": {"offset": 192, "size": 4},
532 | "LoadConfigTableSize": {"offset": 196, "size": 4},
533 | "BoundImportAddress": {"offset": 200, "size": 4},
534 | "BoundImportSize": {"offset": 204, "size": 4},
535 | "ImportAddressTableAddress": {"offset": 208, "size": 4},
536 | "ImportAddressTableSize": {"offset": 212, "size": 4},
537 | "DelayImportDescriptorAddress": {"offset": 216, "size": 4},
538 | "DelayImportDescriptorSize": {"offset": 220, "size": 4},
539 | "COMRuntimeHeaderAddress": {"offset": 224, "size": 4},
540 | "COMRuntimeHeaderSize": {"offset": 228, "size": 4},
541 | "Reserved2": {"offset": 232, "size": 8},
542 | }
543 |
544 | _fields_32 = {
545 | "Magic": {"offset": 0, "size": 2},
546 | "MajorLinkerVersion": {"offset": 2, "size": 1},
547 | "MinorLinkerVersion": {"offset": 3, "size": 1},
548 | "SizeOfCode": {"offset": 4, "size": 4},
549 | "SizeOfInitializedData": {"offset": 8, "size": 4}, #
550 | "SizeOfUninitializedData": {"offset": 12, "size": 4},
551 | "AddressOfEntryPoint": {"offset": 16, "size": 4},
552 | "BaseOfCode": {"offset": 20, "size": 4},
553 | "BaseOfData": {"offset": 24, "size": 4},
554 | "ImageBase": {"offset": 28, "size": 4},
555 | "SectionAlignment": {"offset": 32, "size": 4},
556 | "FileAlignment": {"offset": 36, "size": 4},
557 | "MajorOperatingSystemVersion": {"offset": 40, "size": 2},
558 | "MinorOperatingSystemVersion": {"offset": 42, "size": 2},
559 | "MajorImageVersion": {"offset": 44, "size": 2},
560 | "MinorImageVersion": {"offset": 46, "size": 2},
561 | "MajorSubsystemVersion": {"offset": 48, "size": 2},
562 | "MinorSubsystemVersion": {"offset": 50, "size": 2},
563 | "Reserved": {"offset": 52, "size": 4},
564 | "SizeOfImage": {"offset": 56, "size": 4}, #
565 | "SizeOfHeaders": {"offset": 60, "size": 4},
566 | "CheckSum": {"offset": 64, "size": 4},
567 | "Subsystem": {"offset": 68, "size": 2},
568 | "DLL_Characteristics": {"offset": 70, "size": 2},
569 | "SizeOfStackReserve": {"offset": 72, "size": 4},
570 | "SizeOfStackCommit": {"offset": 76, "size": 4},
571 | "SizeOfHeapReserve": {"offset": 80, "size": 4},
572 | "SizeOfHeapCommit": {"offset": 84, "size": 4},
573 | "LoaderFlags": {"offset": 88, "size": 4},
574 | "NumberOfRvaAndSizes": {"offset": 92, "size": 4},
575 | "ExportTableAddress": {"offset": 96, "size": 4},
576 | "ExportTableSize": {"offset": 100, "size": 4},
577 | "ImportTableAddress": {"offset": 104, "size": 4},
578 | "ImportTableSize": {"offset": 108, "size": 4},
579 | "ResourceTableAddress": {"offset": 112, "size": 4},
580 | "ResourceTableSize": {"offset": 116, "size": 4}, #
581 | "ExceptionTableAddress": {"offset": 120, "size": 4},
582 | "ExceptionTableSize": {"offset": 124, "size": 4},
583 | "CertificateTableAddress": {"offset": 128, "size": 4},
584 | "CertificateTableSize": {"offset": 132, "size": 4},
585 | "BaseRelocationTableAddress": {"offset": 136, "size": 4}, #
586 | "BaseRelocationTableSize": {"offset": 140, "size": 4},
587 | "DebugAddress": {"offset": 144, "size": 4},
588 | "DebugSize": {"offset": 148, "size": 4},
589 | "ArchitectureAddress": {"offset": 152, "size": 4},
590 | "ArchitectureSize": {"offset": 156, "size": 4},
591 | "GlobalPtrAddress": {"offset": 160, "size": 8},
592 | "GlobalPtrSize": {"offset": 168, "size": 0},
593 | "ThreadLocalStorageTableAddress": {"offset": 168, "size": 4},
594 | "ThreadLocalStorageTableSize": {"offset": 172, "size": 4},
595 | "LoadConfigTableAddress": {"offset": 176, "size": 4},
596 | "LoadConfigTableSize": {"offset": 180, "size": 4},
597 | "BoundImportAddress": {"offset": 184, "size": 4},
598 | "BoundImportSize": {"offset": 188, "size": 4},
599 | "ImportAddressTableAddress": {"offset": 192, "size": 4},
600 | "ImportAddressTableSize": {"offset": 196, "size": 4},
601 | "DelayImportDescriptorAddress": {"offset": 200, "size": 4},
602 | "DelayImportDescriptorSize": {"offset": 204, "size": 4},
603 | "COMRuntimeHeaderAddress": {"offset": 208, "size": 4},
604 | "COMRuntimeHeaderSize": {"offset": 212, "size": 4},
605 | "Reserved2": {"offset": 216, "size": 8},
606 | }
607 |
608 | def process_DLL_Characteristics(self, file_data, field_name, field_info):
609 | self.process_Characteristics(file_data, field_name, field_info)
610 |
611 | def process_field(self, file_data, field_name, field_info):
612 | if hasattr(self, "process_" + field_name) and callable(
613 | getattr(self, "process_" + field_name)
614 | ):
615 | getattr(self, "process_" + field_name)(file_data, field_name, field_info)
616 | else:
617 | absolute_offset = field_info["offset"] + self.absolute_offset
618 | size = field_info["size"]
619 | self.size += size
620 | int_value, data = read_bytes(file_data, absolute_offset, size)
621 | field_name_dict = read_from_name_dict(self, field_name)
622 | name = field_name_dict.get(int_value, "")
623 | friendly_name = name.replace("_", " ").capitalize()
624 |
625 | setattr(
626 | self,
627 | field_name,
628 | Structure(
629 | offset=field_info["offset"],
630 | size=size,
631 | value=int_value,
632 | data=data,
633 | absolute_offset=absolute_offset,
634 | name=name,
635 | friendly_name=friendly_name,
636 | ),
637 | )
638 | getattr(self, field_name)._file_data = file_data
639 |
640 | @classmethod
641 | def parse_from_data(cls, file_data, **cls_args):
642 | """Parses the Structure from the file data."""
643 | self = cls(**cls_args)
644 | self._file_data = file_data
645 | magic, x = read_bytes(file_data, self.absolute_offset, 2)
646 |
647 | if magic == _32BIT_MAGIC:
648 | self._fields = self._fields_32
649 | elif magic == _32BIT_PLUS_MAGIC:
650 | self._fields = self._fields_32_plus
651 | else:
652 | print(magic, _32BIT_MAGIC, _32BIT_PLUS_MAGIC)
653 | raise PEFormatError("Magic for Optional Header is invalid.")
654 |
655 | for field_name, field_info in self._fields.items():
656 | self.process_field(file_data, field_name, field_info)
657 |
658 | return self
659 |
660 |
661 | class SectionHeader(Structure):
662 | """Section Header. Each section header is a row in the section table"""
663 |
664 | _fields = {
665 | "Name": {"offset": 0, "size": 8},
666 | "VirtualSize": {"offset": 8, "size": 4}, # .rsrc
667 | "VirtualAddress": {"offset": 12, "size": 4}, # .reloc
668 | "SizeOfRawData": {"offset": 16, "size": 4}, # .rsrc
669 | "PointerToRawData": {"offset": 20, "size": 4}, # .reloc
670 | "PointerToRelocations": {"offset": 24, "size": 4},
671 | "PointerToLineNumbers": {"offset": 28, "size": 4},
672 | "NumberOfRelocations": {"offset": 32, "size": 2},
673 | "NumberOfLineNumbers": {"offset": 34, "size": 2},
674 | "Characteristics": {"offset": 36, "size": 4},
675 | }
676 |
677 |
678 | class ResourceDirectoryTable(Structure):
679 | _fields = {
680 | "Characteristics": {"offset": 0, "size": 4},
681 | "TimeDateStamp": {"offset": 4, "size": 4},
682 | "MajorVersion": {"offset": 8, "size": 2},
683 | "MinorVersion": {"offset": 10, "size": 2},
684 | "NumberOfNameEntries": {"offset": 12, "size": 2},
685 | "NumberOfIDEntries": {"offset": 14, "size": 2},
686 | }
687 |
688 | def __init__(self, *args, **kwargs):
689 | self.name_entries = []
690 | self.id_entries = []
691 | self.subdirectory_tables = []
692 | self.data_entries = []
693 |
694 | super(ResourceDirectoryTable, self).__init__(*args, **kwargs)
695 |
696 |
697 | class ResourceDirectoryEntryName(Structure):
698 | _fields = {
699 | "NameRVA": {"offset": 0, "size": 4},
700 | "DataOrSubdirectoryEntryRVA": {
701 | "offset": 4, # high bit 1 for subdir RVA
702 | "size": 4,
703 | },
704 | }
705 |
706 | directory_string = None
707 |
708 | def is_data_entry(self):
709 | return test_bit(self.DataOrSubdirectoryEntryRVA.value, 31) == 0
710 |
711 | def data_rva_empty(self):
712 | return self.get_data_or_subdirectory_rva() == 0
713 |
714 | def get_data_or_subdirectory_rva(self, virtual_to_physical=0):
715 | return clear_bit(
716 | self.DataOrSubdirectoryEntryRVA.value - virtual_to_physical, 31
717 | )
718 |
719 | def get_data_or_subdirectory_absolute_offset(self):
720 | return (
721 | self.get_data_or_subdirectory_rva()
722 | + self._section_header.PointerToRawData.value
723 | )
724 |
725 | def get_name_absolute_offset(self):
726 | return (
727 | clear_bit(self.NameRVA.value, 31)
728 | + self._section_header.PointerToRawData.value
729 | )
730 |
731 |
732 | class ResourceDirectoryEntryID(Structure):
733 | _fields = {
734 | "IntegerID": {"offset": 0, "size": 4},
735 | "DataOrSubdirectoryEntryRVA": {
736 | "offset": 4, # high bit 1 for Subdir RVA
737 | "size": 4,
738 | },
739 | }
740 |
741 | def is_data_entry(self):
742 | return test_bit(self.DataOrSubdirectoryEntryRVA.value, 31) == 0
743 |
744 | def data_rva_empty(self):
745 | return self.get_data_or_subdirectory_rva() == 0
746 |
747 | def get_data_or_subdirectory_rva(self, virtual_to_physical=0):
748 | return clear_bit(
749 | self.DataOrSubdirectoryEntryRVA.value - virtual_to_physical, 31
750 | )
751 |
752 | def get_data_or_subdirectory_absolute_offset(self, vtp=0):
753 | return (
754 | self.get_data_or_subdirectory_rva(vtp)
755 | + self._section_header.PointerToRawData.value
756 | )
757 |
758 |
759 | class ResourceDirectoryString(Structure):
760 | _fields = {
761 | "Length": {"offset": 0, "size": 2},
762 | # String : offset=2, len=Length
763 | }
764 |
765 | @classmethod
766 | def parse_from_data(cls, file_data, **cls_args):
767 | """Parses the Structure from the file data."""
768 | self = cls(**cls_args)
769 | self._file_data = file_data
770 | str_len, _ = read_bytes(file_data, self.absolute_offset, 2)
771 |
772 | self._fields["String"] = {"offset": 2, "size": str_len}
773 |
774 | for field_name, field_info in self._fields.items():
775 | self.process_field(file_data, field_name, field_info)
776 |
777 | return self
778 |
779 | def process_String(self, file_data, field_name, field_info):
780 | absolute_offset = field_info["offset"] + self.absolute_offset
781 | size = field_info["size"]
782 | self.size += size
783 | data = ""
784 | for i in range(size):
785 | val, dat = read_bytes(file_data, absolute_offset + i * 2, 2)
786 | data += str(dat, "utf-8")
787 |
788 | setattr(
789 | self,
790 | field_name,
791 | Structure(
792 | offset=field_info["offset"],
793 | size=size,
794 | data=data,
795 | absolute_offset=absolute_offset,
796 | ),
797 | )
798 |
799 |
800 | class ResourceDataEntry(Structure):
801 | _fields = {
802 | "DataRVA": {"offset": 0, "size": 4},
803 | "Size": {"offset": 4, "size": 4},
804 | "Codepage": {"offset": 8, "size": 4},
805 | "Reserved": {"offset": 12, "size": 4},
806 | }
807 |
808 | def get_data_absolute_offset(self):
809 | return (
810 | self._section_header.PointerToRawData.value
811 | - self._section_header.VirtualAddress.value
812 | + self.DataRVA.value
813 | )
814 |
815 | def process_field(self, file_data, field_name, field_info):
816 | if hasattr(self, "process_" + field_name) and callable(
817 | getattr(self, "process_" + field_name)
818 | ):
819 | getattr(self, "process_" + field_name)(file_data, field_name, field_info)
820 | else:
821 | absolute_offset = field_info["offset"] + self.absolute_offset
822 | size = field_info["size"]
823 | self.size += size
824 | int_value, data = read_bytes(file_data, absolute_offset, size)
825 | field_name_dict = read_from_name_dict(self, field_name)
826 | name = field_name_dict.get(int_value, "")
827 | friendly_name = name.replace("_", " ").capitalize()
828 |
829 | setattr(
830 | self,
831 | field_name,
832 | Structure(
833 | offset=field_info["offset"],
834 | size=size,
835 | value=int_value,
836 | data=data,
837 | absolute_offset=absolute_offset,
838 | name=name,
839 | friendly_name=friendly_name,
840 | ),
841 | )
842 | getattr(self, field_name)._file_data = file_data
843 |
844 | @classmethod
845 | def parse_from_data(cls, file_data, **cls_args):
846 | """Parses the Structure from the file data."""
847 | self = cls(**cls_args)
848 | self._file_data = file_data
849 | for field_name, field_info in self._fields.items():
850 | self.process_field(file_data, field_name, field_info)
851 |
852 | self.data = read_data(
853 | file_data, self.get_data_absolute_offset(), self.Size.value
854 | )
855 |
856 | return self
857 |
858 |
859 | class ResourceHeader(Structure):
860 | _fields = {
861 | "DataSize": {"offset": 0, "size": 4},
862 | "HeaderSize": {"offset": 4, "size": 4},
863 | "Type": {"offset": 8, "size": 4},
864 | "Name": {"offset": 12, "size": 4},
865 | "DataVersion": {"offset": 16, "size": 4},
866 | "MemoryFlags": {"offset": 20, "size": 2},
867 | "LanguageID": {"offset": 22, "size": 2},
868 | "Version": {"offset": 24, "size": 4},
869 | "Characteristics": {"offset": 28, "size": 4},
870 | }
871 |
872 | def get_name(self):
873 | return resource_types[self.Type.value]
874 |
875 | def set_name(self, value):
876 | for k, v in resource_types.items():
877 | if v == value:
878 | self.Type.value = k
879 | return
880 |
881 |
882 | class IconHeader(Structure):
883 | _fields = {
884 | "Reserved": {"offset": 0, "size": 2},
885 | "ImageType": {"offset": 2, "size": 2}, # 1 for ICO, 2 for CUR, others invalid
886 | "ImageCount": {"offset": 4, "size": 2},
887 | }
888 |
889 | def copy_from(self, group_header):
890 | self.Reserved.value = group_header.Reserved.value
891 | self.ImageType.value = group_header.ResourceType.value
892 | self.ImageCount.value = group_header.ResourceCount.value
893 |
894 | self.entries = []
895 | entry_offset = 0
896 | self.total_size = self.size
897 | for group_entry in group_header.entries:
898 | icon_entry = IconEntry.parse_from_data(
899 | bytearray(""),
900 | absolute_offset=self.absolute_offset + self.size + entry_offset,
901 | offset=entry_offset,
902 | )
903 | icon_entry._file_data = self._file_data
904 | icon_entry.copy_from(group_entry)
905 | icon_entry.number = group_entry.number
906 | self.entries.append(icon_entry)
907 | entry_offset += icon_entry.size
908 | self.total_size += icon_entry.size
909 |
910 | @classmethod
911 | def parse_from_data(cls, file_data, **cls_args):
912 | """Parses the Structure from the file data."""
913 | self = cls(**cls_args)
914 | self._file_data = file_data
915 |
916 | for field_name, field_info in self._fields.items():
917 | self.process_field(file_data, field_name, field_info)
918 |
919 | self.entries = []
920 | entry_offset = 0
921 | self.total_size = self.size
922 | for i in range(self.ImageCount.value):
923 | entry = IconEntry.parse_from_data(
924 | file_data,
925 | absolute_offset=self.absolute_offset + self.size + entry_offset,
926 | offset=entry_offset,
927 | )
928 | entry.number = i + 1
929 | self.entries.append(entry)
930 | entry_offset += entry.size
931 | self.total_size += entry.size
932 |
933 | return self
934 |
935 |
936 | class GroupHeader(Structure):
937 | _fields = {
938 | "Reserved": {"offset": 0, "size": 2},
939 | "ResourceType": {
940 | "offset": 2, # 1 for ICO, 2 for CUR, others invalid
941 | "size": 2,
942 | },
943 | "ResourceCount": {"offset": 4, "size": 2},
944 | }
945 |
946 | def copy_from(self, icon_header):
947 | self.Reserved._file_data = self._file_data
948 | self.ResourceType._file_data = self._file_data
949 | self.ResourceCount._file_data = self._file_data
950 |
951 | self.Reserved.value = icon_header.Reserved.value
952 | self.ResourceType.value = icon_header.ImageType.value
953 | self.ResourceCount.value = icon_header.ImageCount.value
954 |
955 | self.entries = []
956 | entry_offset = 0
957 | self.total_size = self.size
958 | for icon_entry in icon_header.entries:
959 | group_entry = GroupEntry.parse_from_data(
960 | bytearray(b""),
961 | absolute_offset=self.absolute_offset + self.size + entry_offset,
962 | offset=entry_offset,
963 | )
964 | group_entry._file_data = self._file_data
965 | group_entry.copy_from(icon_entry)
966 | group_entry.number = icon_entry.number
967 | self.entries.append(group_entry)
968 | entry_offset += group_entry.size
969 | self.total_size += group_entry.size
970 |
971 | @classmethod
972 | def parse_from_data(cls, file_data, **cls_args):
973 | """Parses the Structure from the file data."""
974 | self = cls(**cls_args)
975 | self._file_data = file_data
976 |
977 | for field_name, field_info in self._fields.items():
978 | self.process_field(file_data, field_name, field_info)
979 |
980 | self.entries = []
981 | entry_offset = 0
982 | self.total_size = self.size
983 | for i in range(self.ResourceCount.value):
984 | entry = GroupEntry.parse_from_data(
985 | file_data,
986 | absolute_offset=self.absolute_offset + self.size + entry_offset,
987 | offset=entry_offset,
988 | )
989 | entry.number = i + 1
990 | self.entries.append(entry)
991 | entry_offset += entry.size
992 | self.total_size += entry.size
993 |
994 | return self
995 |
996 |
997 | class IconEntry(Structure):
998 | _fields = {
999 | "Width": {"offset": 0, "size": 1},
1000 | "Height": {"offset": 1, "size": 1},
1001 | "ColorCount": {"offset": 2, "size": 1},
1002 | "Reserved": {"offset": 3, "size": 1},
1003 | "ColorPlanes": {"offset": 4, "size": 2},
1004 | "BitCount": {"offset": 6, "size": 2}, # bits per pixel
1005 | "DataSize": {"offset": 8, "size": 4},
1006 | "OffsetToData": {"offset": 12, "size": 4}, # from start of file
1007 | }
1008 |
1009 | def copy_from(self, group_entry, entries):
1010 | self.Width.value = group_entry.Width.value
1011 | self.Height.value = group_entry.Height.value
1012 | self.ColorCount.value = group_entry.ColorCount.value
1013 | self.Reserved.value = group_entry.Reserved.value
1014 | self.ColorPlanes.value = group_entry.ColorPlanes.value
1015 | self.BitCount.value = group_entry.BitCount.value
1016 | self.DataSize.value = group_entry.DataSize.value
1017 | self.OffsetToData.value = self._get_entry_offset(group_entry, entries)
1018 |
1019 | def _get_entry_offset(self, group_entry, group_entries):
1020 | offset = 6 # Default icon header size
1021 | offset += self.size * len(group_entries)
1022 |
1023 | for i in range(group_entry.number - 1):
1024 | offset += group_entries[i].DataSize.value
1025 |
1026 | return offset
1027 |
1028 | @classmethod
1029 | def parse_from_data(cls, file_data, **cls_args):
1030 | """Parses the Structure from the file data."""
1031 | self = cls(**cls_args)
1032 | self._file_data = file_data
1033 | for field_name, field_info in self._fields.items():
1034 | self.process_field(file_data, field_name, field_info)
1035 |
1036 | self.data = read_data(file_data, self.OffsetToData.value, self.DataSize.value)
1037 |
1038 | return self
1039 |
1040 |
1041 | class GroupEntry(Structure):
1042 | _fields = {
1043 | "Width": {"offset": 0, "size": 1},
1044 | "Height": {"offset": 1, "size": 1},
1045 | "ColorCount": {"offset": 2, "size": 1},
1046 | "Reserved": {"offset": 3, "size": 1},
1047 | "ColorPlanes": {"offset": 4, "size": 2},
1048 | "BitCount": {"offset": 6, "size": 2},
1049 | "DataSize": {"offset": 8, "size": 4},
1050 | "IconCursorId": {"offset": 12, "size": 2},
1051 | }
1052 |
1053 | def copy_from(self, icon_entry):
1054 | self.Width._file_data = self._file_data
1055 | self.Height._file_data = self._file_data
1056 | self.ColorCount._file_data = self._file_data
1057 | self.Reserved._file_data = self._file_data
1058 | self.ColorPlanes._file_data = self._file_data
1059 | self.BitCount._file_data = self._file_data
1060 | self.DataSize._file_data = self._file_data
1061 | self.IconCursorId._file_data = self._file_data
1062 |
1063 | self.Width.value = icon_entry.Width.value
1064 | self.Height.value = icon_entry.Height.value
1065 | self.ColorCount.value = icon_entry.ColorCount.value
1066 | self.Reserved.value = icon_entry.Reserved.value
1067 | self.ColorPlanes.value = icon_entry.ColorPlanes.value
1068 | self.BitCount.value = icon_entry.BitCount.value
1069 | self.DataSize.value = icon_entry.DataSize.value
1070 | self.IconCursorId.value = icon_entry.number
1071 |
1072 |
1073 | class PEFile(Printable):
1074 | """Reads a portable exe file in either big or little endian.
1075 | Right now this only reads the .rsrc section.
1076 | """
1077 |
1078 | signature = b"MZ"
1079 | dos_header = None
1080 |
1081 | def __init__(self, file_path, endian="little"):
1082 | self.file_path = os.path.abspath(os.path.expanduser(file_path))
1083 |
1084 | self.endian = endian
1085 | if not self.is_PEFile():
1086 | raise PEFormatError(
1087 | "File is not a proper portable executable formatted file!"
1088 | )
1089 |
1090 | self.pe_file_data = bytearray(open(self.file_path, "rb").read())
1091 |
1092 | self.dos_header = DOSHeader.parse_from_data(self.pe_file_data)
1093 | self.pe_header = PEHeader.parse_from_data(
1094 | self.pe_file_data, absolute_offset=self.dos_header.PEHeaderOffset.value
1095 | )
1096 | self.optional_header = OptionalHeader.parse_from_data(
1097 | self.pe_file_data,
1098 | absolute_offset=self.pe_header.size + self.pe_header.absolute_offset,
1099 | )
1100 |
1101 | number_of_sections = self.pe_header.NumberOfSections.value
1102 | section_size = 40
1103 | section_offset = (
1104 | self.pe_header.size
1105 | + self.pe_header.absolute_offset
1106 | + self.pe_header.SizeOfOptionalHeader.value
1107 | )
1108 | self.sections = {}
1109 |
1110 | for section_number in range(number_of_sections):
1111 | section_header = SectionHeader.parse_from_data(
1112 | self.pe_file_data, absolute_offset=section_offset
1113 | )
1114 | section_offset += section_size
1115 | header_name = str(section_header.Name.data, "utf-8").strip("\x00")
1116 | self.sections[header_name] = section_header
1117 |
1118 | if section_header.PointerToLineNumbers.value != 0:
1119 | print(
1120 | "{} section contains line number COFF table, which is not implemented yet.".format(
1121 | section_header.Name
1122 | )
1123 | )
1124 |
1125 | if section_header.PointerToRelocations.value != 0:
1126 | print(
1127 | "{} section contains relocation table, which is not implemented yet.".format(
1128 | section_header.Name
1129 | )
1130 | )
1131 |
1132 | if section_header.Name.data == b".rsrc\x00\x00\x00":
1133 | current_table_pointer = section_header.PointerToRawData.value
1134 | current_resource_directory_table = (
1135 | ResourceDirectoryTable.parse_from_data(
1136 | self.pe_file_data,
1137 | absolute_offset=current_table_pointer,
1138 | _section_header=section_header,
1139 | type=None,
1140 | )
1141 | )
1142 | self.resource_directory_table = current_resource_directory_table
1143 | cur_level = 0
1144 | stack = [(current_resource_directory_table, cur_level)]
1145 |
1146 | delta = (
1147 | section_header.VirtualAddress.value
1148 | - section_header.PointerToRawData.value
1149 | )
1150 |
1151 | while stack:
1152 | resource_directory_table, level = stack.pop()
1153 | num_name_entries = (
1154 | resource_directory_table.NumberOfNameEntries.value
1155 | )
1156 | num_id_entries = resource_directory_table.NumberOfIDEntries.value
1157 | current_offset = (
1158 | resource_directory_table.absolute_offset
1159 | + resource_directory_table.size
1160 | )
1161 |
1162 | for i in range(num_name_entries):
1163 | name_entry = ResourceDirectoryEntryName.parse_from_data(
1164 | self.pe_file_data,
1165 | absolute_offset=current_offset,
1166 | _section_header=section_header,
1167 | )
1168 | current_offset += name_entry.size
1169 |
1170 | string_offset = name_entry.get_name_absolute_offset()
1171 | name_entry.directory_string = (
1172 | ResourceDirectoryString.parse_from_data(
1173 | self.pe_file_data,
1174 | absolute_offset=string_offset,
1175 | _section_header=section_header,
1176 | )
1177 | )
1178 |
1179 | offset = name_entry.get_data_or_subdirectory_absolute_offset()
1180 |
1181 | if not name_entry.data_rva_empty():
1182 | if name_entry.is_data_entry():
1183 | rd = ResourceDataEntry.parse_from_data(
1184 | self.pe_file_data,
1185 | absolute_offset=offset,
1186 | _section_header=section_header,
1187 | )
1188 | resource_directory_table.data_entries.append(rd)
1189 | else:
1190 | rd = ResourceDirectoryTable.parse_from_data(
1191 | self.pe_file_data,
1192 | absolute_offset=offset,
1193 | _section_header=section_header,
1194 | type=None,
1195 | )
1196 | resource_directory_table.subdirectory_tables.append(rd)
1197 |
1198 | stack.append((rd, level + 1))
1199 |
1200 | resource_directory_table.name_entries.append(name_entry)
1201 |
1202 | for i in range(num_id_entries):
1203 | id_entry = ResourceDirectoryEntryID.parse_from_data(
1204 | self.pe_file_data,
1205 | absolute_offset=current_offset,
1206 | _section_header=section_header,
1207 | )
1208 | current_offset += id_entry.size
1209 |
1210 | offset = id_entry.get_data_or_subdirectory_absolute_offset()
1211 |
1212 | if id_entry.is_data_entry():
1213 | rd = ResourceDataEntry.parse_from_data(
1214 | self.pe_file_data,
1215 | absolute_offset=offset,
1216 | _section_header=section_header,
1217 | )
1218 | resource_directory_table.data_entries.append(rd)
1219 | else:
1220 | id_entry.name = str(id_entry.IntegerID.value)
1221 | if level + 1 == 1:
1222 | id_entry.name = resource_types[id_entry.IntegerID.value]
1223 | rd = ResourceDirectoryTable.parse_from_data(
1224 | self.pe_file_data,
1225 | absolute_offset=offset,
1226 | _section_header=section_header,
1227 | type=id_entry.IntegerID.value,
1228 | )
1229 | resource_directory_table.subdirectory_tables.append(rd)
1230 | stack.append((rd, level + 1))
1231 | resource_directory_table.id_entries.append(id_entry)
1232 |
1233 | def replace_icon(self, icon_path):
1234 | """Replaces an icon in the pe file with the one specified.
1235 | This only replaces the largest icon and resizes the input
1236 | image to match so that the data is undisturbed. I tried to
1237 | update the pointers automatically by moving the data to the end
1238 | of the file, but that did not work. Comments were left as history
1239 | to what I attempted.
1240 | """
1241 | icon_path = os.path.expanduser(
1242 | icon_path
1243 | ) # this needs to be a string and not unicode
1244 |
1245 | if not os.path.exists(icon_path):
1246 | raise Exception("Icon {} does not exist".format(icon_path))
1247 |
1248 | resource_section = self.sections[".rsrc"]
1249 |
1250 | g_icon_dir = self.get_directory_by_type(ResourceTypes.Group_Icon)
1251 | g_icon_data_entry = g_icon_dir.subdirectory_tables[0].data_entries[0]
1252 | icon_dir = self.get_directory_by_type(ResourceTypes.Icon)
1253 | icon_data_entry = icon_dir.subdirectory_tables[0].data_entries[0]
1254 |
1255 | group_header = GroupHeader.parse_from_data(
1256 | self.pe_file_data,
1257 | absolute_offset=g_icon_data_entry.get_data_absolute_offset(),
1258 | )
1259 | g_entry = group_header.entries[0]
1260 |
1261 | icon = Image.open(icon_path)
1262 | width = g_entry.Width.value
1263 | height = g_entry.Height.value
1264 |
1265 | if width == 0:
1266 | width = 256
1267 | if height == 0:
1268 | height = 256
1269 |
1270 | i_data = resize(icon, (width, height), format="ico")
1271 |
1272 | new_icon_size = len(i_data)
1273 | icon_file_size = g_entry.DataSize.value + group_header.size + g_entry.size + 2
1274 |
1275 | # 9662 is the exact length of the icon in nw.exe
1276 | extra_size = icon_file_size - new_icon_size
1277 |
1278 | if extra_size < 0:
1279 | extra_size = 0
1280 | icon_data = bytearray(i_data) + bytearray(extra_size)
1281 |
1282 | icon_header = IconHeader.parse_from_data(icon_data, absolute_offset=0)
1283 |
1284 | # group_header.absolute_offset = len(self.pe_file_data)
1285 | # g_icon_data_entry.DataRVA.value = len(self.pe_file_data) - resource_section.PointerToRawData.value + resource_section.VirtualAddress.value
1286 | # padding = 6+14*len(icon_header.entries)
1287 | # g_icon_data_entry.Size.value = padding
1288 |
1289 | # self.pe_file_data += bytearray(padding)
1290 | group_header._file_data = self.pe_file_data
1291 | group_header.copy_from(icon_header)
1292 |
1293 | # icon_data_entry.DataRVA.value = len(self.pe_file_data) - resource_section.PointerToRawData.value + resource_section.VirtualAddress.value
1294 | # print hex(icon_data_entry.DataRVA.value), hex(len(self.pe_file_data)), hex(icon_data_entry.get_data_absolute_offset())
1295 |
1296 | # print hex(read_bytes(self.pe_file_data[icon_data_entry.DataRVA.absolute_offset:icon_data_entry.DataRVA.absolute_offset+icon_data_entry.DataRVA.size],0, icon_data_entry.DataRVA.size)[0])
1297 |
1298 | # data = bytearray()
1299 | # for entry in icon_header.entries:
1300 | # data += entry.data
1301 | # self.pe_file_data += entry.data
1302 |
1303 | # icon_data_entry.Size.value = len(data)
1304 |
1305 | # self.optional_header.SizeOfImage.value = self.optional_header.SizeOfImage.value + len(data) + padding
1306 | # self.optional_header.ResourceTableSize.value = self.optional_header.ResourceTableSize.value + len(data) + padding
1307 | # self.optional_header.SizeOfInitializedData.value = self.optional_header.SizeOfInitializedData.value + len(data) + padding
1308 | # resource_section.SizeOfRawData.value = resource_section.SizeOfRawData.value + len(data) + padding
1309 | # resource_section.VirtualSize.value = resource_section.VirtualSize.value + len(data) + padding
1310 | # print icon_header.total_size
1311 | data_address = icon_data_entry.get_data_absolute_offset()
1312 | data_size = icon_data_entry.Size.value
1313 | self.pe_file_data = (
1314 | self.pe_file_data[:data_address]
1315 | + icon_data[icon_header.total_size :]
1316 | + self.pe_file_data[data_address + data_size :]
1317 | )
1318 |
1319 | def write(self, file_name):
1320 | with open(file_name, "wb+") as f:
1321 | f.write(self.pe_file_data)
1322 |
1323 | def get_directory_by_type(self, type):
1324 | """Gets the directory by resource type."""
1325 | for d in self.resource_directory_table.subdirectory_tables:
1326 | if d.type == type:
1327 | return d
1328 |
1329 | def is_PEFile(self):
1330 | """Checks if the file is a proper PE file"""
1331 | signature = None
1332 | try:
1333 | with open(self.file_path, "rb") as f:
1334 | signature = f.read(2)
1335 | except IOError as e:
1336 | raise e
1337 | finally:
1338 | return signature == self.signature
1339 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | appdirs==1.4.4
2 | black==24.8.0
3 | configobj==5.0.9
4 | pillow==10.4.0
5 | pyinstaller==6.10.0
6 | pylint==3.3.0
7 | pyside6==6.7.2
8 | pytest==8.3.3
9 | requests==2.32.3
10 | semantic_version==2.10.0
11 | validators==0.34.0
12 |
--------------------------------------------------------------------------------
/scripts/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleIconFile
5 | icon-windowed.icns
6 | CFBundlePackageType
7 | APPL
8 | CFBundleIdentifier
9 | Web2Executable
10 | CFBundleName
11 | Web2Executable
12 | CFBundleShortVersionString
13 | 0.6.0
14 | CFBundleExecutable
15 | MacOS/Web2Executable
16 | CFBundleDisplayName
17 | Web2Executable
18 | CFBundleInfoDictionaryVersion
19 | 6.0
20 | LSBackgroundOnly
21 | 0
22 | NSPrincipalClass
23 | NSApplication
24 |
25 |
26 |
--------------------------------------------------------------------------------
/scripts/Web2Exe.nsi:
--------------------------------------------------------------------------------
1 | ; Script generated by the HM NIS Edit Script Wizard.
2 |
3 | ; HM NIS Edit Wizard helper defines
4 | !define PRODUCT_NAME "Web2Exe"
5 | !define /file PRODUCT_VERSION "..\files\version.txt"
6 | !define PRODUCT_PUBLISHER "SimplyPixelated"
7 | !define PRODUCT_WEB_SITE "http://www.simplypixelated.com"
8 | !define PRODUCT_DIR_REGKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\${PRODUCT_NAME}.exe"
9 | !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
10 | !define PRODUCT_UNINST_ROOT_KEY "HKLM"
11 | !define PRODUCT_STARTMENU_REGVAL "NSIS:StartMenuDir"
12 |
13 | SetCompressor lzma
14 |
15 | ; MUI 1.67 compatible ------
16 | !include "MUI.nsh"
17 |
18 | ; MUI Settings
19 | !define MUI_ABORTWARNING
20 | !define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico"
21 | !define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico"
22 |
23 | ; Welcome page
24 | !insertmacro MUI_PAGE_WELCOME
25 | ; License page
26 | !insertmacro MUI_PAGE_LICENSE "..\license.rtf"
27 | ; Components page
28 | !insertmacro MUI_PAGE_COMPONENTS
29 | ; Directory page
30 | !insertmacro MUI_PAGE_DIRECTORY
31 | ; Start menu page
32 | var ICONS_GROUP
33 | !define MUI_STARTMENUPAGE_NODISABLE
34 | !define MUI_STARTMENUPAGE_DEFAULTFOLDER "${PRODUCT_NAME}"
35 | !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${PRODUCT_UNINST_ROOT_KEY}"
36 | !define MUI_STARTMENUPAGE_REGISTRY_KEY "${PRODUCT_UNINST_KEY}"
37 | !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${PRODUCT_STARTMENU_REGVAL}"
38 | !insertmacro MUI_PAGE_STARTMENU Application $ICONS_GROUP
39 | ; Instfiles page
40 | !insertmacro MUI_PAGE_INSTFILES
41 | ; Finish page
42 | !insertmacro MUI_PAGE_FINISH
43 |
44 | ; Uninstaller pages
45 | !insertmacro MUI_UNPAGE_INSTFILES
46 |
47 | ; Language files
48 | !insertmacro MUI_LANGUAGE "English"
49 |
50 | ; Reserve files
51 | !insertmacro MUI_RESERVEFILE_INSTALLOPTIONS
52 |
53 | ; MUI end ------
54 |
55 | Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
56 | OutFile "..\Web2Exe-Setup.exe"
57 |
58 | RequestExecutionLevel admin ;Require admin rights on NT6+ (When UAC is turned on)
59 |
60 | !include LogicLib.nsh
61 |
62 | Function .onInit
63 | UserInfo::GetAccountType
64 | pop $0
65 | ${If} $0 != "admin" ;Require admin rights on NT4+
66 | MessageBox mb_iconstop "Administrator rights required!"
67 | SetErrorLevel 740 ;ERROR_ELEVATION_REQUIRED
68 | Quit
69 | ${EndIf}
70 | FunctionEnd
71 |
72 | InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
73 | InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" ""
74 | ShowInstDetails show
75 | ShowUnInstDetails show
76 |
77 | Section "Web2Executable" SEC01
78 | SectionIn RO
79 | SetOutPath "$INSTDIR"
80 | SetOverwrite try
81 | File /r /x compressors "..\Web2ExeWin\"
82 |
83 | CreateDirectory "$LocalAppData\Web2Executable\Web2Executable\files\compressors"
84 | SetOutPath "$LocalAppData\Web2Executable\Web2Executable\files\compressors"
85 | File "..\Web2ExeWin\files\compressors\upx-win.exe"
86 |
87 | ; Shortcuts
88 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application
89 | CreateDirectory "$SMPROGRAMS\$ICONS_GROUP"
90 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
91 | CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
92 | !insertmacro MUI_STARTMENU_WRITE_END
93 | SectionEnd
94 |
95 | LangString DESC_SEC01 ${LANG_ENGLISH} "A tool to convert web applications to exe's."
96 |
97 | !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
98 | !insertmacro MUI_DESCRIPTION_TEXT ${SEC01} $(DESC_SEC01)
99 | !insertmacro MUI_FUNCTION_DESCRIPTION_END
100 |
101 | Section -AdditionalIcons
102 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application
103 | WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${PRODUCT_WEB_SITE}"
104 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Website.lnk" "$INSTDIR\${PRODUCT_NAME}.url"
105 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Uninstall.lnk" "$INSTDIR\uninst.exe"
106 | !insertmacro MUI_STARTMENU_WRITE_END
107 | SectionEnd
108 |
109 | Section -Post
110 | WriteUninstaller "$INSTDIR\uninst.exe"
111 | WriteRegStr HKLM "${PRODUCT_DIR_REGKEY}" "" "$INSTDIR\${PRODUCT_NAME}.exe"
112 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)"
113 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe"
114 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_NAME}.exe"
115 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
116 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
117 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
118 | SectionEnd
119 |
120 |
121 | Function un.onUninstSuccess
122 | HideWindow
123 | MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) was successfully removed from your computer."
124 | FunctionEnd
125 |
126 | Function un.onInit
127 | MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to completely remove $(^Name) and all of its components?" IDYES +2
128 | Abort
129 | FunctionEnd
130 |
131 | Section Uninstall
132 | RMDir /r "$INSTDIR"
133 | Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk"
134 | Delete "$SMPROGRAMS\${PRODUCT_NAME}\Website.lnk"
135 | Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
136 | Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk"
137 |
138 | DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}"
139 | DeleteRegKey HKLM "${PRODUCT_DIR_REGKEY}"
140 | SetAutoClose true
141 | SectionEnd
142 |
--------------------------------------------------------------------------------
/scripts/build_command_line_linux.bash:
--------------------------------------------------------------------------------
1 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
2 |
3 | README=""
4 | PROJ_DIR=$DIR
5 |
6 | while [[ -z $README ]] && [[ $PROJ_DIR != "/" ]]; do
7 | README=$(find $PROJ_DIR -maxdepth 1 -name "README.md")
8 |
9 | if [[ -z $README ]]; then
10 | PROJ_DIR="$(dirname $PROJ_DIR)"
11 | fi
12 | done
13 |
14 | if [[ $PROJ_DIR == "/" ]]; then
15 | echo "No suitable project directory was found. Exiting."
16 | exit 1
17 | fi
18 |
19 | BUILD_DIR="$PROJ_DIR/Web2ExeBuild"
20 |
21 | ## Remove old build directories
22 | rm -rf $PROJ_DIR/build $BUILD_DIR
23 | rm -rf $PROJ_DIR/Web2ExeLinux-*
24 |
25 | VERSION=$(cat $PROJ_DIR/files/version.txt)
26 |
27 | ################# Build CMD Version ###################
28 |
29 | pyinstaller --onefile --exclude-module PyQt5 --exclude-module PyQt4 \
30 | --hidden-import PIL.Jpeg2KImagePlugin \
31 | --hidden-import pkg_resources \
32 | --hidden-import PIL._imaging \
33 | --hidden-import configobj \
34 | --distpath $BUILD_DIR/Web2ExeLinux-CMD \
35 | -n web2exe-linux $PROJ_DIR/command_line.py
36 |
37 | cp -rf $PROJ_DIR/files $BUILD_DIR/Web2ExeLinux-CMD/files
38 |
39 | ## Remove any unneeded files
40 | rm -rf $BUILD_DIR/Web2ExeLinux-CMD/files/downloads/*
41 | rm $BUILD_DIR/Web2ExeLinux-CMD/files/error.log \
42 | $BUILD_DIR/Web2ExeLinux-CMD/files/last_project_path.txt \
43 | $BUILD_DIR/Web2ExeLinux-CMD/files/recent_files.txt \
44 | $BUILD_DIR/Web2ExeLinux-CMD/files/compressors/upx-mac \
45 | $BUILD_DIR/Web2ExeLinux-CMD/files/compressors/upx-win.exe
46 |
47 | ################# Build GUI Version ###################
48 |
49 | pyinstaller -F --exclude-module PyQt5 --exclude-module PyQt4 \
50 | --hidden-import PIL.Jpeg2KImagePlugin \
51 | --hidden-import configobj \
52 | --hidden-import PIL._imaging \
53 | --hidden-import pkg_resources \
54 | -n web2exe --distpath $BUILD_DIR/Web2ExeLinux $PROJ_DIR/main.py
55 |
56 | ## Copy the files directory over
57 | cp -rf $PROJ_DIR/files $BUILD_DIR/Web2ExeLinux/files
58 |
59 | ## Remove any unneeded files
60 | rm -rf $BUILD_DIR/Web2ExeLinux/files/downloads/*
61 | rm $BUILD_DIR/Web2ExeLinux/files/error.log \
62 | $BUILD_DIR/Web2ExeLinux/files/last_project_path.txt \
63 | $BUILD_DIR/Web2ExeLinux/files/recent_files.txt \
64 | $BUILD_DIR/Web2ExeLinux/files/compressors/upx-mac \
65 | $BUILD_DIR/Web2ExeLinux/files/compressors/upx-win.exe
66 |
67 |
68 | ################# Zip and Upload to Github ###################
69 |
70 | cd $BUILD_DIR
71 |
72 | zip -r -9 $PROJ_DIR/Web2ExeLinux-CMD.zip Web2ExeLinux-CMD/*
73 | zip -r -9 $PROJ_DIR/Web2ExeLinux-${VERSION}.zip Web2ExeLinux
74 |
75 | cd -
76 |
77 | python3.4 $DIR/upload_release.py
78 |
--------------------------------------------------------------------------------
/scripts/build_mac.bash:
--------------------------------------------------------------------------------
1 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
2 |
3 | README=""
4 | PROJ_DIR=$DIR
5 |
6 | while [[ -z $README ]] && [[ $PROJ_DIR != "/" ]]; do
7 | README=$(find $PROJ_DIR -maxdepth 1 -name "README.md")
8 |
9 | if [[ -z $README ]]; then
10 | PROJ_DIR="$(dirname $PROJ_DIR)"
11 | fi
12 | done
13 |
14 | if [[ $PROJ_DIR == "/" ]]; then
15 | echo "No suitable project directory was found. Exiting."
16 | exit 1
17 | fi
18 |
19 | BUILD_DIR="$PROJ_DIR/Web2ExeBuild"
20 |
21 | ## Remove old build directories
22 | rm -rf $PROJ_DIR/build $BUILD_DIR
23 |
24 | VERSION=$(cat $PROJ_DIR/files/version.txt)
25 |
26 | ################# Build CMD Version ###################
27 |
28 | pyinstaller --hidden-import PIL.Jpeg2KImagePlugin \
29 | --hidden-import configobj \
30 | --hidden-import pkg_resources \
31 | --distpath $BUILD_DIR/Web2ExeMac-CMD \
32 | --onefile -n web2exe-mac $PROJ_DIR/command_line.py
33 |
34 | CMD_FILES_DIR=$BUILD_DIR/Web2ExeMac-CMD/files
35 |
36 | cp -rf $PROJ_DIR/files $CMD_FILES_DIR
37 |
38 | rm -rf $CMD_FILES_DIR/downloads/*
39 | rm $CMD_FILES_DIR/error.log \
40 | $CMD_FILES_DIR/last_project_path.txt \
41 | $CMD_FILES_DIR/recent_files.txt \
42 | $CMD_FILES_DIR/compressors/upx-linux-x64 \
43 | $CMD_FILES_DIR/compressors/upx-linux-x32 \
44 | $CMD_FILES_DIR/compressors/upx-win.exe
45 |
46 | rm -rf $PROJ_DIR/build $PROJ_DIR/dist
47 |
48 | ################# Build GUI Version ###################
49 |
50 | pyinstaller -w --hidden-import PIL.Jpeg2KImagePlugin \
51 | --hidden-import PyQt4 \
52 | --hidden-import PIL \
53 | --hidden-import configobj \
54 | --hidden-import pkg_resources \
55 | --distpath $BUILD_DIR/ \
56 | --onefile -n Web2Executable $PROJ_DIR/main.py
57 |
58 | FILES_DIR=$BUILD_DIR/Web2Executable.app/Contents/MacOS/files
59 |
60 | cp $PROJ_DIR/images/icon.icns $BUILD_DIR/Web2Executable.app/Contents/Resources/icon-windowed.icns
61 | cp -rf $PROJ_DIR/files $FILES_DIR
62 |
63 | rm -rf $FILES_DIR/downloads/*
64 | rm $FILES_DIR/error.log \
65 | $FILES_DIR/last_project_path.txt \
66 | $FILES_DIR/recent_files.txt \
67 | $FILES_DIR/compressors/upx-linux-x64 \
68 | $FILES_DIR/compressors/upx-linux-x32 \
69 | $FILES_DIR/compressors/upx-win.exe
70 |
71 | rm -rf $PROJ_DIR/build $PROJ_DIR/dist
72 |
73 | ################# Zip and Upload to Github ###################
74 |
75 | rm -rf Web2ExeMac*.zip
76 |
77 | cp $PROJ_DIR/scripts/Info.plist $BUILD_DIR/Web2Executable.app/Contents/
78 |
79 | /Applications/Keka.app/Contents/Resources/keka7z a -r \
80 | Web2ExeMac-CMD.zip $BUILD_DIR/Web2ExeMac-CMD
81 |
82 | /Applications/Keka.app/Contents/Resources/keka7z a -r \
83 | Web2ExeMac-${VERSION}.zip $BUILD_DIR/Web2Executable.app
84 |
85 | python3 $DIR/upload_release.py
86 |
--------------------------------------------------------------------------------
/scripts/build_windows.bat:
--------------------------------------------------------------------------------
1 | rd /S /Q Web2ExeWin
2 | rd /S /Q build
3 | del *.zip
4 | call pyinstaller --onefile ^
5 | --hidden-import PIL.Jpeg2KImagePlugin ^
6 | --hidden-import configobj ^
7 | --hidden-import pkg_resources ^
8 | -i images\icon.ico ^
9 | --distpath Web2ExeWin-CMD ^
10 | -n web2exe-win command_line.py
11 |
12 | rd /S /Q Web2ExeWin-CMD\files
13 | echo D | xcopy /s files Web2ExeWin-CMD\files
14 |
15 |
16 | call pyinstaller -w --onefile ^
17 | --hidden-import PIL.Jpeg2KImagePlugin ^
18 | --hidden-import pkg_resources ^
19 | --hidden-import configobj ^
20 | -i images\icon.ico ^
21 | --distpath Web2ExeWin -n Web2Exe main.py
22 |
23 | echo D | xcopy /s files Web2ExeWin\files
24 |
25 | del Web2ExeWin\files\compressors\upx-mac
26 | del Web2ExeWin\files\compressors\upx-linux-x64
27 | del Web2ExeWin\files\compressors\upx-linux-x32
28 |
29 | del Web2ExeWin-CMD\files\compressors\upx-mac
30 | del Web2ExeWin-CMD\files\compressors\upx-linux-x64
31 | del Web2ExeWin-CMD\files\compressors\upx-linux-x32
32 |
33 | makensis /V4 scripts/Web2Exe.nsi
34 |
35 | set /p Version= 0
102 |
103 |
104 | def test_download_nwjs(command_base):
105 | command_base.get_setting("nw_version").value = "0.19.0"
106 | command_base.get_setting("windows-x64").value = True
107 | command_base.init()
108 | command_base.get_files_to_download()
109 |
110 | command_base.download_file_with_error_handling()
111 |
112 | base, _ = os.path.split(__file__)
113 |
114 | assert os.path.exists(
115 | utils.path_join(
116 | base, "test_data", "files", "downloads", "nwjs-v0.19.0-win-x64.zip"
117 | )
118 | )
119 |
--------------------------------------------------------------------------------
/util_classes.py:
--------------------------------------------------------------------------------
1 | """Utility classes that are used both in the GUI and the CMD."""
2 |
3 | import os
4 | import re
5 | from fnmatch import fnmatch
6 | import zipfile
7 | import tarfile
8 | import time
9 | import logging
10 | from pprint import pformat
11 |
12 | import config
13 | import utils
14 |
15 | from PySide6 import QtGui, QtCore
16 | from PySide6 import QtWidgets
17 | from PySide6.QtCore import Qt
18 |
19 |
20 | class FileItem(QtWidgets.QTreeWidgetItem):
21 | def __init__(self, parent=None, path=None):
22 | super(FileItem, self).__init__(parent)
23 | self.path = path
24 |
25 |
26 | class FileTree(object):
27 | def __init__(self, directory=None, whitelist=None, blacklist=None):
28 | self.whitelist = None
29 | self.blacklist = None
30 |
31 | self.paths = []
32 | self.walkcache = {}
33 | self.cache = True
34 | self.time = time.time()
35 |
36 | self.files = []
37 | self.dirs = []
38 |
39 | self.init(directory, whitelist, blacklist)
40 |
41 | def init(self, directory=None, whitelist=None, blacklist=None):
42 | self.logger = config.getLogger(__name__)
43 |
44 | if directory:
45 | self.directory = directory + os.sep
46 | else:
47 | self.directory = directory
48 |
49 | self.refresh(whitelist, blacklist)
50 |
51 | def clear(self):
52 | self.files = []
53 | self.dirs = []
54 |
55 | def refresh(self, whitelist=None, blacklist=None):
56 | self.set_filters(whitelist, blacklist)
57 |
58 | self.clear()
59 |
60 | self.generate_files()
61 |
62 | def walk(self, directory):
63 | refresh = False
64 |
65 | if (time.time() - self.time) > 10:
66 | refresh = True
67 | self.time = time.time()
68 |
69 | if not self.walkcache.get(directory) or refresh:
70 | self.walkcache[directory] = []
71 | return os.walk(directory)
72 |
73 | return self.walkcache[directory]
74 |
75 | def determine_skip(self, path, *args, **kwargs):
76 | skip = False
77 |
78 | for blacklist in self.blacklist:
79 | match = fnmatch(path, blacklist)
80 | if match:
81 | skip = True
82 | self.on_blacklist_match(path, *args, **kwargs)
83 | break
84 |
85 | for whitelist in self.whitelist:
86 | match = fnmatch(path, whitelist)
87 | if match:
88 | skip = False
89 | self.on_whitelist_match(path, *args, **kwargs)
90 | break
91 |
92 | return skip
93 |
94 | def set_filters(self, whitelist=None, blacklist=None):
95 | self.whitelist = whitelist or self.whitelist or []
96 | self.blacklist = blacklist or self.blacklist or []
97 |
98 | def on_whitelist_match(self, path, *args, **kwargs):
99 | pass
100 |
101 | def on_blacklist_match(self, path, *args, **kwargs):
102 | pass
103 |
104 | def init_cache(self):
105 | if self.walkcache.get(self.directory) is None:
106 | self.walkcache[self.directory] = []
107 |
108 | self.cache = False
109 | if not self.walkcache[self.directory]:
110 | self.cache = True
111 |
112 | def add_to_cache(self, *args):
113 | if self.cache:
114 | self.walkcache[self.directory].append(args)
115 |
116 | def is_in_skipped(self, skipped, path):
117 | temp = path
118 |
119 | while temp:
120 | if temp in skipped:
121 | return True
122 | if temp == os.path.dirname(temp):
123 | return False
124 | temp = os.path.dirname(temp)
125 | return False
126 |
127 | def generate_files(self):
128 | if self.directory is None:
129 | return
130 |
131 | self.logger.debug("Blacklist pattern:")
132 | self.logger.debug(pformat(self.blacklist))
133 | self.logger.debug("")
134 | self.logger.debug("Whitelist pattern:")
135 | self.logger.debug(pformat(self.whitelist))
136 | self.logger.debug("")
137 |
138 | self.init_cache()
139 |
140 | skipped_files = set()
141 |
142 | for root, dirs, files in self.walk(self.directory):
143 | self.add_to_cache(root, dirs, files)
144 |
145 | proj_path = root.replace(self.directory, "")
146 |
147 | for directory in dirs:
148 | path = os.path.join(proj_path, directory)
149 |
150 | if self.determine_skip(path):
151 | if not self.is_in_skipped(skipped_files, path):
152 | self.logger.debug("Skipping dir: %s", path)
153 | skipped_files.add(path)
154 | continue
155 | else:
156 | if self.is_in_skipped(skipped_files, path):
157 | self.logger.debug("Keeping dir: %s", path)
158 |
159 | self.dirs.append(path)
160 |
161 | for file in files:
162 | path = os.path.join(proj_path, file)
163 |
164 | if self.determine_skip(path):
165 | if not self.is_in_skipped(skipped_files, path):
166 | self.logger.debug("Skipping file: %s", path)
167 | skipped_files.add(path)
168 | continue
169 | else:
170 | if self.is_in_skipped(skipped_files, path):
171 | self.logger.debug("Keeping file: %s", path)
172 |
173 | self.files.append(path)
174 |
175 |
176 | class TreeBrowser(QtWidgets.QWidget):
177 | def __init__(self, directory=None, whitelist=None, blacklist=None, parent=None):
178 | QtWidgets.QWidget.__init__(self, parent=parent)
179 | self.root = QtWidgets.QTreeWidget()
180 | self.root.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
181 | self.root.header().setStretchLastSection(False)
182 | self.root.setHeaderLabel("Included files")
183 |
184 | self.parent_map = {}
185 |
186 | layout = QtWidgets.QVBoxLayout()
187 | layout.setContentsMargins(0, 0, 0, 0)
188 | layout.addWidget(self.root)
189 | self.setLayout(layout)
190 |
191 | self.file_tree = FileTree(directory, whitelist, blacklist)
192 | self.init(directory, whitelist, blacklist)
193 |
194 | def clear(self):
195 | self.root.clear()
196 | self.file_tree.clear()
197 |
198 | def init(self, directory=None, whitelist=None, blacklist=None):
199 | self.file_tree.init(directory, whitelist, blacklist)
200 |
201 | self.refresh(whitelist, blacklist)
202 |
203 | def refresh(self, whitelist=None, blacklist=None):
204 | self.file_tree.set_filters(whitelist, blacklist)
205 | self.clear()
206 | self.generate_files()
207 |
208 | def fix_tree(self, path, parent):
209 | temp = parent
210 | tree = []
211 | root = None
212 | base, direc = os.path.split(path)
213 | new_path = os.path.join(base, direc)
214 |
215 | while new_path and root is None:
216 | temp = self.parent_map.get(os.path.join(base, direc))
217 | if temp is not None:
218 | root = temp
219 | break
220 |
221 | child = FileItem(None, os.path.join(base, direc))
222 | child.setText(0, direc)
223 |
224 | tree.insert(0, child)
225 | base, direc = os.path.split(base)
226 | new_path = os.path.join(base, direc)
227 |
228 | self.parent_map[child.path] = child
229 |
230 | # if we reached the top of the directory chain
231 | if root is None:
232 | # if the path is still valid, that means we need to create
233 | # a new top level item
234 | if new_path:
235 | root = FileItem(None, new_path)
236 | root.setText(0, direc)
237 | self.parent_map[root.path] = root
238 | else:
239 | # otherwise, the if the path is empty, we already
240 | # have a top level item, so use that
241 | root = tree.pop(0)
242 |
243 | self.root.addTopLevelItem(root)
244 |
245 | # add all the children to the root node
246 | temp = root
247 | for child in tree:
248 | temp.addChild(child)
249 | temp = child
250 |
251 | def on_whitelist_match(self, path, parent=None):
252 | if parent is None:
253 | self.fix_tree(path, parent)
254 |
255 | def generate_files(self):
256 | directory = self.file_tree.directory
257 |
258 | if directory is None:
259 | return
260 |
261 | self.parent_map = {"": self.root}
262 |
263 | self.file_tree.init_cache()
264 |
265 | for root, dirs, files in self.file_tree.walk(directory):
266 | self.file_tree.add_to_cache(root, dirs, files)
267 |
268 | proj_path = root.replace(directory, "")
269 |
270 | for dir in dirs:
271 | parent = self.parent_map.get(proj_path)
272 |
273 | path = os.path.join(proj_path, dir)
274 |
275 | if self.file_tree.determine_skip(path, parent=parent) or parent is None:
276 | continue
277 |
278 | child = FileItem(parent, path)
279 | child.setText(0, dir)
280 | self.parent_map[path] = child
281 | self.file_tree.dirs.append(path)
282 |
283 | for file in files:
284 | parent = self.parent_map.get(proj_path)
285 |
286 | path = os.path.join(proj_path, file)
287 |
288 | if self.file_tree.determine_skip(path, parent=parent) or parent is None:
289 | continue
290 |
291 | child = FileItem(parent, path)
292 | child.setText(0, file)
293 | self.file_tree.files.append(path)
294 |
295 | self.root.sortItems(0, Qt.AscendingOrder)
296 |
297 |
298 | class ExistingProjectDialog(QtWidgets.QDialog):
299 | def __init__(self, recent_projects, directory_callback, parent=None):
300 | super(ExistingProjectDialog, self).__init__(parent)
301 | self.setWindowTitle("Open Project Folder")
302 | self.setWindowIcon(QtGui.QIcon(config.get_file("files/images/icon.png")))
303 | self.setMinimumWidth(500)
304 |
305 | group_box = QtWidgets.QGroupBox("Existing Projects")
306 | gbox_layout = QtWidgets.QVBoxLayout()
307 | self.project_list = QtWidgets.QListWidget()
308 |
309 | gbox_layout.addWidget(self.project_list)
310 | group_box.setLayout(gbox_layout)
311 |
312 | self.callback = directory_callback
313 |
314 | self.projects = recent_projects
315 |
316 | for project in recent_projects:
317 | text = "{} - {}".format(os.path.basename(project), project)
318 | self.project_list.addItem(text)
319 |
320 | self.project_list.itemClicked.connect(self.project_clicked)
321 |
322 | self.cancel = QtWidgets.QPushButton("Cancel")
323 | self.open = QtWidgets.QPushButton("Open Selected")
324 | self.open_readonly = QtWidgets.QPushButton("Open Read-only")
325 | self.browse = QtWidgets.QPushButton("Browse...")
326 |
327 | self.open.setEnabled(False)
328 | self.open.clicked.connect(self.open_clicked)
329 |
330 | self.open_readonly.setEnabled(False)
331 | self.open_readonly.clicked.connect(self.open_readonly_clicked)
332 |
333 | self.browse.clicked.connect(self.browse_clicked)
334 |
335 | buttons = QtWidgets.QWidget()
336 |
337 | button_layout = QtWidgets.QHBoxLayout()
338 | button_layout.addWidget(self.cancel)
339 | button_layout.addWidget(QtWidgets.QWidget())
340 | button_layout.addWidget(self.browse)
341 | button_layout.addWidget(self.open_readonly)
342 | button_layout.addWidget(self.open)
343 |
344 | buttons.setLayout(button_layout)
345 |
346 | layout = QtWidgets.QVBoxLayout()
347 | layout.addWidget(group_box)
348 | layout.addWidget(buttons)
349 |
350 | self.setLayout(layout)
351 | self.cancel.clicked.connect(self.cancelled)
352 |
353 | def browse_clicked(self):
354 | default = self.parent().project_dir() or self.parent().last_project_dir
355 |
356 | directory = QtWidgets.QFileDialog.getExistingDirectory(
357 | self, "Find Project Directory", default
358 | )
359 |
360 | if directory:
361 | self.callback(directory)
362 | self.close()
363 |
364 | def open_clicked(self):
365 | pos = self.project_list.currentRow()
366 | self.callback(self.projects[pos])
367 | self.close()
368 |
369 | def open_readonly_clicked(self):
370 | pos = self.project_list.currentRow()
371 | self.callback(self.projects[pos], readonly=True)
372 | self.close()
373 |
374 | def project_clicked(self, _):
375 | self.open.setEnabled(True)
376 | self.open_readonly.setEnabled(True)
377 |
378 | def cancelled(self):
379 | self.close()
380 |
381 |
382 | class Validator(QtGui.QRegularExpressionValidator):
383 | def __init__(self, regex, action, parent=None):
384 | self.exp = regex
385 | self.action = str
386 | if hasattr(str, action):
387 | self.action = getattr(str, action)
388 | reg = QtCore.QRegularExpression(regex)
389 | super(Validator, self).__init__(reg, parent)
390 |
391 | def validate(self, text, pos):
392 | result = super(Validator, self).validate(text, pos)
393 | return result
394 |
395 | def fixup(self, text):
396 | return "".join(re.findall(self.exp, self.action(text)))
397 |
398 |
399 | class BackgroundThread(QtCore.QThread):
400 | def __init__(self, widget, method_name, parent=None):
401 | QtCore.QThread.__init__(self, parent)
402 | self.widget = widget
403 | self.method_name = method_name
404 |
405 | def run(self):
406 | if hasattr(self.widget, self.method_name):
407 | func = getattr(self.widget, self.method_name)
408 | func()
409 |
410 |
411 | class Setting(object):
412 | """Class that describes a setting from the setting.cfg file"""
413 |
414 | def __init__(
415 | self,
416 | name="",
417 | display_name=None,
418 | value=None,
419 | required=False,
420 | type=None,
421 | file_types=None,
422 | *args,
423 | **kwargs
424 | ):
425 | self.name = name
426 | self.display_name = (
427 | display_name if display_name else name.replace("_", " ").capitalize()
428 | )
429 | self.value = value
430 | self.last_value = None
431 | self.required = required
432 | self.type = type
433 | self.url = kwargs.pop("url", "")
434 | self.copy = kwargs.pop("copy", True)
435 | self.file_types = file_types
436 | self.scope = kwargs.pop("scope", "local")
437 |
438 | self.default_value = kwargs.pop("default_value", None)
439 | self.button = kwargs.pop("button", None)
440 | self.button_callback = kwargs.pop("button_callback", None)
441 | self.description = kwargs.pop("description", "")
442 | self.values = kwargs.pop("values", [])
443 | self.filter = kwargs.pop("filter", ".*")
444 | self.filter_action = kwargs.pop("filter_action", "None")
445 | self.check_action = kwargs.pop("check_action", "None")
446 | self.action = kwargs.pop("action", None)
447 |
448 | self.set_extra_attributes_from_keyword_args(**kwargs)
449 |
450 | if self.value is None:
451 | self.value = self.default_value
452 |
453 | self.save_path = kwargs.pop("save_path", "")
454 |
455 | self.get_file_information_from_url()
456 |
457 | def filter_name(self, text):
458 | """Use the filter action to filter out invalid text"""
459 | if text and hasattr(self.filter_action, text):
460 | action = getattr(self.filter_action, text)
461 | return action(text)
462 | return text or ""
463 |
464 | def get_file_information_from_url(self):
465 | """Extract the file information from the setting url"""
466 | if hasattr(self, "url"):
467 | self.file_name = self.url.split("/")[-1]
468 | self.full_file_path = utils.path_join(self.save_path, self.file_name)
469 | self.file_ext = os.path.splitext(self.file_name)[1]
470 | if self.file_ext == ".zip":
471 | self.extract_class = zipfile.ZipFile
472 | self.extract_args = ()
473 | elif self.file_ext == ".gz":
474 | self.extract_class = tarfile.TarFile.open
475 | self.extract_args = ("r:gz",)
476 |
477 | def save_file_path(self, version, location=None, sdk_build=False):
478 | """Get the save file path based on the version"""
479 | if location:
480 | self.save_path = location
481 | else:
482 | self.save_path = location or config.download_path()
483 |
484 | self.get_file_information_from_url()
485 |
486 | if self.full_file_path:
487 | path = self.full_file_path.format(version)
488 |
489 | if sdk_build:
490 | path = utils.replace_right(path, "nwjs", "nwjs-sdk", 1)
491 |
492 | return path
493 |
494 | return ""
495 |
496 | def set_extra_attributes_from_keyword_args(self, **kwargs):
497 | for undefined_key, undefined_value in kwargs.items():
498 | setattr(self, undefined_key, undefined_value)
499 |
500 | def extract(self, ex_path, version, location=None, sdk_build=False):
501 | if os.path.exists(ex_path):
502 | utils.rmtree(ex_path, ignore_errors=True)
503 |
504 | path = location or self.save_file_path(version, sdk_build=sdk_build)
505 |
506 | file = self.extract_class(path, *self.extract_args)
507 | # currently, python's extracting mechanism for zipfile doesn't
508 | # copy file permissions, resulting in a binary that
509 | # that doesn't work. Copied from a patch here:
510 | # http://bugs.python.org/file34873/issue15795_cleaned.patch
511 | if path.endswith(".zip"):
512 | members = file.namelist()
513 | for zipinfo in members:
514 | minfo = file.getinfo(zipinfo)
515 | target = file.extract(zipinfo, ex_path)
516 | mode = minfo.external_attr >> 16 & 0x1FF
517 | os.chmod(target, mode)
518 | else:
519 | file.extractall(ex_path)
520 |
521 | if path.endswith(".tar.gz"):
522 | dir_name = utils.path_join(
523 | ex_path, os.path.basename(path).replace(".tar.gz", "")
524 | )
525 | else:
526 | dir_name = utils.path_join(
527 | ex_path, os.path.basename(path).replace(".zip", "")
528 | )
529 |
530 | if os.path.exists(dir_name):
531 | for p in os.listdir(dir_name):
532 | abs_file = utils.path_join(dir_name, p)
533 | utils.move(abs_file, ex_path)
534 | utils.rmtree(dir_name, ignore_errors=True)
535 |
536 | def __repr__(self):
537 | url = ""
538 | if hasattr(self, "url"):
539 | url = self.url
540 | return (
541 | "Setting: (name={}, "
542 | "display_name={}, "
543 | "value={}, required={}, "
544 | "type={}, url={})"
545 | ).format(
546 | self.name, self.display_name, self.value, self.required, self.type, url
547 | )
548 |
549 |
550 | class CompleterLineEdit(QtWidgets.QLineEdit):
551 | def __init__(self, tag_dict, *args):
552 | QtWidgets.QLineEdit.__init__(self, *args)
553 |
554 | self.pref = ""
555 | self.tag_dict = tag_dict
556 |
557 | def text_changed(self, text):
558 | all_text = str(text)
559 | text = all_text[: self.cursorPosition()]
560 | prefix = re.split(r"(?<=\))(.*)(?=%\()", text)[-1].strip()
561 | self.pref = prefix
562 | if prefix.strip() != prefix:
563 | self.pref = ""
564 |
565 | def complete_text(self, text):
566 | cursor_pos = self.cursorPosition()
567 | before_text = str(self.text())[:cursor_pos]
568 | after_text = str(self.text())[cursor_pos:]
569 | prefix_len = len(re.split(r"(?<=\))(.*)(?=%\()", before_text)[-1].strip())
570 | tag_text = self.tag_dict.get(text)
571 |
572 | if tag_text is None:
573 | tag_text = text
574 |
575 | new_text = "{}{}{}".format(
576 | before_text[: cursor_pos - prefix_len], tag_text, after_text
577 | )
578 | self.setText(new_text)
579 | self.setCursorPosition(len(new_text))
580 |
581 |
582 | class TagsCompleter(QtWidgets.QCompleter):
583 | def __init__(self, parent, all_tags):
584 | self.keys = sorted(all_tags.keys())
585 | self.vals = sorted([val for val in all_tags.values()])
586 | self.tags = list(sorted(self.vals + self.keys))
587 | QtWidgets.QCompleter.__init__(self, self.tags, parent)
588 | self.editor = parent
589 |
590 | def update(self, text):
591 | obj = self.editor
592 | completion_prefix = obj.pref
593 | model = QtWidgets.QStringListModel(self.tags, self)
594 | self.setModel(model)
595 |
596 | self.setCompletionPrefix(completion_prefix)
597 | if completion_prefix.strip() != "":
598 | self.complete()
599 |
--------------------------------------------------------------------------------
/utils.py:
--------------------------------------------------------------------------------
1 | """Utility functions for Web2Executable
2 |
3 | This module holds utility functions that are useful to both the command line
4 | and GUI modules, but aren't related to either module.
5 | """
6 | from __future__ import print_function
7 | import os
8 | import zipfile
9 | import io
10 | import platform
11 | import urllib.request as request
12 | import tempfile
13 | import codecs
14 | import shutil
15 | import subprocess
16 | from appdirs import AppDirs
17 | import validators
18 | import traceback
19 | import logging
20 | import config
21 |
22 | from PySide6 import QtCore
23 |
24 | logger = logging.getLogger(__name__)
25 |
26 |
27 | def url_exists(path):
28 | if validators.url(path):
29 | return True
30 | return False
31 |
32 |
33 | def format_exc_info(exc_info):
34 | """Return exception string with traceback"""
35 | exc_format = traceback.format_exception(exc_info[0], exc_info[1], exc_info[2])
36 | error = "".join([x for x in exc_format])
37 | return error
38 |
39 |
40 | def load_last_project_path():
41 | """Load the last open project.
42 |
43 | Returns:
44 | string: the last opened project path
45 | """
46 | proj_path = ""
47 | proj_file = get_data_file_path(config.LAST_PROJECT_FILE)
48 | if os.path.exists(proj_file):
49 | with codecs.open(proj_file, encoding="utf-8") as f:
50 | proj_path = f.read().strip()
51 |
52 | if not proj_path:
53 | proj_path = QtCore.QDir.currentPath()
54 |
55 | return proj_path
56 |
57 |
58 | def load_recent_projects():
59 | """Load the most recent projects opened.
60 |
61 | Returns:
62 | list: project files sorted by most recent
63 | """
64 | files = []
65 | history_file = get_data_file_path(config.RECENT_FILES_FILE)
66 | if not os.path.exists(history_file):
67 | return files
68 | with codecs.open(history_file, encoding="utf-8") as f:
69 | for line in f:
70 | line = line.strip()
71 | if line and os.path.exists(line):
72 | files.append(line)
73 | files.reverse()
74 | return files
75 |
76 |
77 | def save_project_path(path):
78 | """Save the last open project path."""
79 | proj_file = get_data_file_path(config.LAST_PROJECT_FILE)
80 | with codecs.open(proj_file, "w+", encoding="utf-8") as f:
81 | f.write(path)
82 |
83 |
84 | def save_recent_project(proj):
85 | """Save the most recent projects to a text file."""
86 | recent_file_path = get_data_file_path(config.RECENT_FILES_FILE)
87 | max_length = config.MAX_RECENT
88 | recent_files = []
89 | if os.path.exists(recent_file_path):
90 | file_contents = codecs.open(recent_file_path, encoding="utf-8").read()
91 | recent_files = file_contents.split("\n")
92 | try:
93 | recent_files.remove(proj)
94 | except ValueError:
95 | pass
96 | recent_files.append(proj)
97 | with codecs.open(recent_file_path, "w+", encoding="utf-8") as f:
98 | for recent_file in recent_files[-max_length:]:
99 | if recent_file and os.path.exists(recent_file):
100 | f.write("{}\n".format(recent_file))
101 |
102 |
103 | def replace_right(source, target, replacement, replacements=None):
104 | """
105 | String replace rightmost instance of a string.
106 |
107 | Args:
108 | source (string): the source to perform the replacement on
109 | target (string): the string to search for
110 | replacement (string): the replacement string
111 | replacements (int or None): if an integer, only replaces N occurrences
112 | otherwise only one occurrence is replaced
113 | """
114 | return replacement.join(source.rsplit(target, replacements))
115 |
116 |
117 | def is_windows():
118 | return platform.system() == "Windows"
119 |
120 |
121 | def get_temp_dir():
122 | return tempfile.gettempdir()
123 |
124 |
125 | ## File operations ------------------------------------------------------
126 | # These are overridden because shutil gets Windows directories confused
127 | # and cannot write to them even if they are valid in cmd.exe
128 |
129 |
130 | def path_join(base, *rest):
131 | new_rest = []
132 | for r in rest:
133 | new_rest.append(str(r))
134 |
135 | rpath = "/".join(new_rest)
136 |
137 | if not os.path.isabs(rpath):
138 | rpath = base + "/" + rpath
139 |
140 | if is_windows():
141 | rpath = rpath.replace("/", "\\")
142 |
143 | rpath = os.path.normpath(rpath)
144 |
145 | return rpath
146 |
147 |
148 | def get_data_path(dir_path):
149 | parts = dir_path.split("/")
150 | if config.TESTING:
151 | data_path = path_join(config.CWD, "tests", "test_data", *parts)
152 | else:
153 | dirs = AppDirs("Web2Executable", "Web2Executable")
154 | data_path = path_join(dirs.user_data_dir, *parts)
155 |
156 | if is_windows():
157 | data_path = data_path.replace("\\", "/")
158 |
159 | if not os.path.exists(data_path):
160 | os.makedirs(data_path)
161 |
162 | return data_path
163 |
164 |
165 | def abs_path(file_path):
166 | path = os.path.abspath(file_path)
167 |
168 | if is_windows():
169 | path = path.replace("/", "\\")
170 |
171 | return path
172 |
173 |
174 | def get_data_file_path(file_path):
175 | parts = file_path.split("/")
176 | data_path = get_data_path("/".join(parts[:-1]))
177 | return path_join(data_path, parts[-1])
178 |
179 |
180 | def rmtree(path, **kwargs):
181 | if is_windows():
182 | if os.path.isabs(path):
183 | path = "\\\\?\\" + path.replace("/", "\\")
184 | shutil.rmtree(path, **kwargs)
185 |
186 |
187 | def copy(src, dest, **kwargs):
188 | if is_windows():
189 | if os.path.isabs(src):
190 | src = "\\\\?\\" + src.replace("/", "\\")
191 | if os.path.isabs(dest):
192 | dest = "\\\\?\\" + dest.replace("/", "\\")
193 | shutil.copy2(src, dest, **kwargs)
194 |
195 |
196 | def move(src, dest, **kwargs):
197 | if is_windows():
198 | if os.path.isabs(src):
199 | src = "\\\\?\\" + src.replace("/", "\\")
200 | if os.path.isabs(dest):
201 | dest = "\\\\?\\" + dest.replace("/", "\\")
202 | shutil.move(src, dest, **kwargs)
203 |
204 |
205 | def copytree(src, dest, **kwargs):
206 | if is_windows():
207 | if os.path.isabs(src) and not src.startswith("\\\\"):
208 | src = "\\\\?\\" + src.replace("/", "\\")
209 | if os.path.isabs(dest) and not dest.startswith("\\\\"):
210 | dest = "\\\\?\\" + dest.replace("/", "\\")
211 | shutil.copytree(src, dest, **kwargs)
212 |
213 |
214 | ## ------------------------------------------------------------
215 |
216 |
217 | def log(*args):
218 | """Print logging information or log it to a file."""
219 | if config.DEBUG:
220 | print(*args)
221 | logger.info(", ".join(args))
222 |
223 |
224 | def open_folder_in_explorer(path):
225 | """Cross platform open folder window."""
226 | if platform.system() == "Windows":
227 | os.startfile(path)
228 | elif platform.system() == "Darwin":
229 | subprocess.Popen(["open", path])
230 | else:
231 | subprocess.Popen(["xdg-open", path])
232 |
233 |
234 | def zip_files(zip_file_name, project_dir, *args, **kwargs):
235 | """
236 | Zip files into an archive programmatically.
237 |
238 | Args:
239 | zip_file_name (string): the name of the resulting zip file
240 | args: the files to zip
241 | kwargs: Options
242 | verbose (bool): if True, gives verbose output
243 | exclude_paths (list): a list of paths to exclude
244 | """
245 | zip_file = zipfile.ZipFile(zip_file_name, "w", config.ZIP_MODE)
246 | verbose = kwargs.pop("verbose", False)
247 | old_path = os.getcwd()
248 |
249 | os.chdir(project_dir)
250 |
251 | for arg in args:
252 | if os.path.exists(arg):
253 | file_loc = arg
254 | if verbose:
255 | log(file_loc)
256 | try:
257 | zip_file.write(file_loc)
258 | except ValueError:
259 | os.utime(file_loc, None)
260 | zip_file.write(file_loc)
261 |
262 | os.chdir(old_path)
263 |
264 | zip_file.close()
265 |
266 |
267 | def join_files(destination, *args, **kwargs):
268 | """
269 | Join any number of files together by stitching bytes together.
270 |
271 | This is used to take advantage of NW.js's ability to execute a zip file
272 | contained at the end of the exe file.
273 |
274 | Args:
275 | destination (string): the name of the resulting file
276 | args: the files to stitch together
277 | """
278 | with io.open(destination, "wb") as dest_file:
279 | for arg in args:
280 | if os.path.exists(arg):
281 | with io.open(arg, "rb") as file:
282 | while True:
283 | bytes = file.read(4096)
284 | if len(bytes) == 0:
285 | break
286 | dest_file.write(bytes)
287 |
288 |
289 | def urlopen(url):
290 | """
291 | Call urllib.request.urlopen with a modified SSL context to prevent
292 | "SSL: CERTIFICATE_VERIFY_FAILED” errors when no verification is
293 | actually needed.
294 | """
295 | return request.urlopen(url, context=config.SSL_CONTEXT)
296 |
297 |
298 | # To avoid a circular import, we import config at the bottom of the file
299 | # and reference it on the module level from within the functions
300 | import config
301 |
--------------------------------------------------------------------------------