├── .github └── workflows │ └── python-package.yml ├── README.md ├── excelAddinGenerator ├── __init__.py └── main.py ├── setup.py ├── src └── data │ ├── [Content_Types].xml │ ├── _rels │ └── .rels │ ├── docProps │ ├── app.xml │ └── core.xml │ └── xl │ ├── _rels │ └── workbook.xml.rels │ ├── styles.xml │ ├── theme │ └── theme1.xml │ ├── workbook.xml │ └── worksheets │ └── sheet1.xml └── tests ├── __init__.py ├── blank.bin ├── test.xlam ├── test_excelAddinGenerator.py └── vbaProject.bin /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.8", "3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi 33 | - name: Lint with flake8 34 | run: | 35 | # stop the build if there are Python syntax errors or undefined names 36 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 37 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 38 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 39 | - name: Test with pytest 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | run: | 43 | pytest --cov=excelAddinGenerator 44 | coveralls --service=github 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Excel-Addin-Generator 2 | Tools to create an Excel Addin from VBA code 3 | 4 | [![Coverage Status](https://coveralls.io/repos/github/Beakerboy/Excel-Addin-Generator/badge.svg?branch=master)](https://coveralls.io/github/Beakerboy/Excel-Addin-Generator?branch=master) 5 | ![Build Status](https://github.com/Beakerboy/Excel-Addin-Generator/actions/workflows/python-package.yml/badge.svg) 6 | 7 | 8 | Preparation 9 | ------------ 10 | 11 | An Excel Addin (.xlam) is a zip archive of XML and binary files. Some of these XML files contain information specific to the computer that was used to create the xlam file. This script can be used to create a standard xlam file from the VBA code. 12 | 13 | The script can be given a source xlam file, from which the binary file is extracted and repackaged, or the binary file can be provided alone. 14 | 15 | **Usage** 16 | 17 | `python excelAddinGenerator/main.py path/to/vbaProject.bin output/to/myAddin.xlam` 18 | 19 | or 20 | 21 | `python excelAddinGenerator/main.py path/to/source.xlam output/to/myAddin.xlam` 22 | -------------------------------------------------------------------------------- /excelAddinGenerator/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /excelAddinGenerator/main.py: -------------------------------------------------------------------------------- 1 | import shutil, sys, os, zipfile 2 | 3 | def main(args): 4 | if len(args) > 2: 5 | # check the extension on sys.argv[1] to determine which function to call 6 | input_file = args[1] 7 | output_file_name = args[2] 8 | if input_file.endswith('.xlam'): 9 | createFromZip(input_file, args[0] + '/../src/data', output_file_name) 10 | elif input_file.endswith('.bin'): 11 | createFromBin(input_file, args[0] + '/../src/data', output_file_name) 12 | else: 13 | raise Exception(input_file, " is not a valid file format.") 14 | 15 | def createFromBin(input_file, wrapper_dir, output_file_name): 16 | """Create a zip file containing the provided bin""" 17 | # file must start with 'd0 cf 11 e0 a1 b1 1a e1' 18 | fileSig = open(input_file, "rb").read(8).hex() 19 | if fileSig != 'd0cf11e0a1b11ae1': 20 | raise Exception('File signature {} is not as expected.', format(fileSig)) 21 | shutil.copy(input_file, wrapper_dir + "/xl/vbaProject.bin") 22 | shutil.make_archive(output_file_name, 'zip', wrapper_dir) 23 | shutil.move(output_file_name + ".zip", output_file_name) 24 | 25 | def createFromZip(input_file, wrapper_dir, output_file_name): 26 | """Create a zip file containing the bin file within the provided zip file""" 27 | extractBinFromZip(input_file) 28 | createFromBin('xl/vbaProject.bin', wrapper_dir, output_file_name) 29 | 30 | def extractBinFromZip(input_file): 31 | # check that input is a zip file 32 | if zipfile.is_zipfile(input_file): 33 | # check that the zip archive contains /xl/vbaProject.bin 34 | with zipfile.ZipFile(input_file, 'r') as zip: 35 | zip.extract('xl/vbaProject.bin') 36 | else: 37 | raise Exception(input_file, " is not a valid file format.") 38 | 39 | if __name__ == "__main__": 40 | args = [] 41 | args[0] = os.path.dirname(sys.argv[0]) 42 | args[1] = sys.argv[1] 43 | args[2] = sys.argv[2] 44 | main(args) 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="excelAddinGenerator", 5 | packages=['excelAddinGenerator'], 6 | tests_require=['pytest'], 7 | ) 8 | -------------------------------------------------------------------------------- /src/data/[Content_Types].xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/_rels/.rels: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/data/docProps/app.xml: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Excel0falseWorksheets1Sheet1Beakerboy Softwarefalsefalsefalse16.0300 -------------------------------------------------------------------------------- /src/data/docProps/core.xml: -------------------------------------------------------------------------------- 1 | 2 | Excel Addin GeneratorExcel Addin Generator2019-04-24T20:22:02Z2019-04-24T20:22:28Z -------------------------------------------------------------------------------- /src/data/xl/_rels/workbook.xml.rels: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/xl/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/xl/theme/theme1.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/xl/workbook.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/data/xl/worksheets/sheet1.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/blank.bin: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/test.xlam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beakerboy/Excel-Addin-Generator/2d43b06e17531eb14fc58c9d0a677ebb2214e3c8/tests/test.xlam -------------------------------------------------------------------------------- /tests/test_excelAddinGenerator.py: -------------------------------------------------------------------------------- 1 | # test_excelAddinGenerator.py 2 | 3 | import pytest 4 | from excelAddinGenerator.main import * 5 | from os.path import exists 6 | from filehash import FileHash 7 | 8 | def test_success_from_bin(): 9 | """Test that xlam is successfully generated from a OLE file""" 10 | createFromBin("tests/vbaProject.bin", "src/data", "success_bin.xlam") 11 | # Assert that xlam file is created 12 | assert exists("success_bin.xlam") 13 | #assert that bin file within success_bin.xlam matches tests/vbaProject.bin 14 | extractBinFromZip("success_bin.xlam") 15 | md5hasher = FileHash('md5') 16 | assert md5hasher.hash_file("tests/vbaProject.bin") == md5hasher.hash_file("xl/vbaProject.bin") 17 | 18 | createFromZip("success_bin.xlam", "src/data", "success_xlam.xlam") 19 | assert exists("success_xlam.xlam") 20 | #assert that bin file within success_xlam.xlam matches bin file within success_bin.xlam 21 | extractBinFromZip("success_xlam.xlam") 22 | assert md5hasher.hash_file("tests/vbaProject.bin") == md5hasher.hash_file("xl/vbaProject.bin") 23 | 24 | def test_not_bin_exception(): 25 | """ Test that an exception is thrown if the bin file is not an OLE file""" 26 | with pytest.raises(Exception) as e_info: 27 | createFromBin("tests/blank.bin", "src/data", "./fail.xlam") 28 | 29 | def test_xlam_not_zip(): 30 | """ Test that an exception is thrown if the zip is not a zip archive""" 31 | with pytest.raises(Exception) as e_info: 32 | createFromZip("tests/blank.bin", "src/data", "./fail.xlam") 33 | 34 | def test_main(): 35 | main(["./excelAddinGenerator", "./tests/vbaProject.bin", "success_bin.xlam"]) 36 | main(["./excelAddinGenerator", "success_bin.xlam", "success_xlam.xlam"]) 37 | 38 | def test_main_incorrect_type(): 39 | """ Test that an exception is thrown if the zip is not a zip archive""" 40 | with pytest.raises(Exception) as e_info: 41 | main(["./excelAddinGenerator", "./src/data/xl/styles.xml", "fail.xlam"]) 42 | -------------------------------------------------------------------------------- /tests/vbaProject.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Beakerboy/Excel-Addin-Generator/2d43b06e17531eb14fc58c9d0a677ebb2214e3c8/tests/vbaProject.bin --------------------------------------------------------------------------------