Version control for Hacking Kinetic Base Layer Zip files. BuildKineticApplication.bat

Sometimes, it’s easier to edit raw JSON than to use the available application studio tools, such as when you want to reorganize columns in a grid. If I’ve missed the functionality, feel free to tell me, but currently, the only way I’ve been able to accomplish this is to delete all columns and re-add them in the new order.

Enter Export. You can export the base layers of your custom apps, extract them, modify the RAW JSON files, re-archive them, and then import them back into Kinetic.

This method has a few downsides: It’s cumbersome, has no version control, and overwrites all version history for the layer in Epicor Kinetic.

Here is my contribution to overcoming some of those challenges with a simple tool.

What it does:

  1. Identifies base Kinetic Layers in the .\Apps\Erp.UI.<APP NAME>
  2. Find the latest major and minor version numbers from the zip in the current folders. .<App Name>_#_#.zip and determines the file name for a new archive by incriminating the revision number by 1.
  3. Compares the previous version to the current in .\Apps\Erp.UI.<APP NAME> by extracting the files to a temp folder and comparing timestamps.
  4. Archives the current version if it has been modified from the previous revision.
  5. It then performs some cleanup and reports what it has done.

How to use it:

Export your layer from Epicor Kinetic Application Studio and extract the zip contents into the folder containing the BuildKineticApplication.bat batch file.

Execute the BuildKineticApplication.bat file or rename the MetaUIs.zip file to something more identifiable to create your first ‘backup.’

Modify the JSON files and save your changes.

Execute the BuildKineticApplication.bat file to create an importable kinetic base layer.

Import the modified layer and test, revert, or repeat as necessary.

This makes it easy to archive any base kinetic layers you may be hacking while giving you some peace of mind that you can revert to a previous iteration.

Additional caveats: It’s not perfect, it doesn’t do everything I would like, it’s not well documented, it’s being released as is, and should you choose to use it, you do so at your own risk.

Let me know your thoughts or comments.

@REM BuildKineticApplication.bat
@REM Version control for Base Layer Epicor Kinetic Applications
@REM 
@REM By Kevin Barrow released: 10/25/2024

@echo off
setlocal enabledelayedexpansion

if exist !SystemDrive!\Temp ( 
	set "TEMP=!SYSTEMDRIVE!\Temp"
)

REM Define the base folder and file name
set "baseFolder=%CD%"
set "layerName="

REM Loop through each subfolder in the base folder(s)
for /D %%D in ("layers\*","Apps\*") do (
    REM Reset the file counter and incremental value for each subfolder
    set "MajorVersion=0"
    set "Revision=0"
    set "layerPath=%%~dpD"
	set "layerFolder=%%~nxD"
    REM for %%i in ("!layerPath!") do set "layerType=%%~nxi"
	for %%i in ("!layerPath:~0,-1!") do set "layerType=%%~nxi"
	echo !layerType! !layerFolder! !layerPath!
	echo *******************************************************
    for /f "tokens=3 delims=." %%a in ("%%~nxD") do (
        set "layerName=%%a"

        REM Find the largest file number in the current subfolder
        for %%F in ("!layerName!_*.zip") do (
            for /F "tokens=2 delims=_" %%V in ("%%~nF") do (
                if %%V gtr !MajorVersion! (
                    set "MajorVersion=%%V"
                    set /A "PrevRevision=!Revision!"
                    set "Revision=0"
                ) else if %%V equ !MajorVersion! (
                    set "PrevVersion=!MajorVersion!"
                    REM for /R ".\" %%G in ("!layerName!_!MajorVersion!_*.zip") do (
                    for /R ".\" %%G in ("!layerName!_!MajorVersion!_*.zip") do (
                        for /F "tokens=3 delims=_" %%R in ("%%~nG") do (
                            if %%R geq !Revision! (
                                set /A "PrevRevision=%%R"
                                set /A "Revision=%%R + 1"
                            )
							REM echo .
							REM echo fileIteration: %%F %%G Version: !MajorVersion!  verIteration: %%V  nextRev: !Revision!  revIteration: %%R
                        )
						REM echo fileIteration: %%F %%G Version: !MajorVersion!  verIteration: %%V  nextRev: !Revision!
                    )
                )
            )
		)

		REM Create the previous and new file name
		set newFileName=!layerName!_!MajorVersion!_!Revision!
		if defined PrevVersion if defined PrevRevision (
			set "prevFileName=!layerName!_!PrevVersion!_!PrevRevision!"
		)
		echo newFileName: !newFileName! PrevFileName: !prevFileName! Version: !MajorVersion! Revision: !Revision!

		if not exist "!newFileName!.zip" (
			REM check for existing newFileName before proceeding
			
			if exist "!prevFileName!.zip" (
				set "prevTempDir=!TEMP!\!prevFileName!"

				echo .
				echo Create a temporary directory for comparison !prevTempDir!
				mkdir "!prevTempDir!" 2>nul
				
				REM Extract the existing zip file to the temporary directory
				powershell -Command "Expand-Archive -Path '!prevFileName!.zip' -DestinationPath '!prevTempDir!' -Force"
				
				REM Compare the contents of the folder with the temporary directory
				set "changed=0"
				echo ---------------------------------------------------
				echo %%D
				pushd %CD%\%%D
				for /R %%G in (*) do (
					set "fullPath=%%G"
					set "relativePath=!fullPath:%baseFolder%=!"
					if "!relative_path:~0,1!"== "\" set "relative_path=!relative_path:~1!"
					echo debug -- fileIteration: %%G  !currentFileDate! compareLocation: !fileInTemp!  !tempFileDate!  relativePath=!relativePath!
					set "fileInTemp=!prevTempDir!\!relativePath!"
					if not exist "!fileInTemp!" (
						set "changed=1"
						echo File %%G is new or modified. tempFile: !fileInTemp!
					) else (
						REM Compare modification timestamps using WMIC for accurate comparison
						for %%H in ("!fileInTemp!") do (
							set currentFileDate=%%~tG
							set tempFileDate=%%~tH
							if not defined currentFileDate (
								set "changed=1"
								echo no currentFileDate obtained for %%G
							) else if not defined tempFileDate (
								set "changed=1"
								echo no datestamp obtained for %%H
							) else if !currentFileDate! NEQ !tempFileDate! (
								set "changed=1"
								echo File %%~nxG is modified.
							)
							REM echo fileIteration: %%G  !currentFileDate! compareLocation: !fileInTemp!  prevFileDate: !tempFileDate!
						)
					)
				)
				popd
				if !changed! equ 0 ( echo No changes detected in %%D  )
				echo ---------------------------------------------------
				echo Clean up the temporary directory !prevTempDir!
				rmdir /s /q "!prevTempDir!"
			
			) else ( set "changed=1" )


			REM If there are changes, create the new zip file
			if !changed! equ 1 (
				set "tempDir=!TEMP!\!newFileName!" 
				echo Creating temp directory for Structure Archival !tempDir!\
				
				mkdir !tempDir!\!layerType!\!layerFolder!
				
				echo Copying !CD!\!layerType!\!layerFolder! to !tempDir!\!layerType!\
				echo xcopy /e !CD!\!layerType!\!layerFolder! !tempDir!\!layerType!\!layerFolder! /Q
				xcopy /e "!CD!\!layerType!\!layerFolder!" "!tempDir!\!layerType!\!layerFolder!" /Q

				echo Creating new zip file: !newFileName!.zip
				PowerShell -Command "Compress-Archive -Path '!Temp!\!newFileName!\*' -DestinationPath '!CD!\!newFileName!.zip' -Update -CompressionLevel Optimal"
				
				REM echo "Execution paused for debugging, !tempDir!\ contains proposed content for !CD!\!newFileName!.zip"
				REM echo "press <Ctrl+C> to abort and save or <Enter> to delete"
				REM pause
				
				echo cleaning up temp files !tempDir!
				rmdir /s /q !tempDir!
			) else (
				echo No changes detected in %%D, skipping...
			)


		) else (
			echo Zip file !newFileName!.zip already exists, skipping...
		)
	)
)
endlocal
2 Likes

Sorry, I’m giggling at the forwardness of this.
You will use it.

4 Likes

Ya, kinda blunt…maybe something a little more lowkey like:

“…and (should you choose to use it) you will be doing so at entirely your own risk.”

1 Like

So, I guess I’m trying to understand the need for this.
Couldn’t you just extract the file and name it _date.zip or whatever it extracts?
I don’t do much with batch files, but it seems like it’s just renaming them and putting them into a folder structure or am I read it incorrectly?

1 Like

You could probably PowerShell that whole damn thing.

2 Likes

YARN | Your mission, should you choose to accept it ...

and you will like it.

1 Like

Comedy Horror GIF by Dead Meat James

Thank you for sharing your tool, it’s given me a few ideas already.

Whether I ever get to them is a different story.

3 Likes

Kevin meet Kevin LOL

@Mark_Wonsil powershell, python, Cobol, c#, perl, lisp, turbo pascal… And the list goes on.

1 Like

Couldn’t resist
Here’s a documented version from our AI friend… I have not bothered to read it. but

Summary
@REM BuildKineticApplication.bat
@REM Version control for Base Layer Epicor Kinetic Applications
@REM 
@REM By Kevin Barrow released: 10/25/2024

@REM This script automates the version control process for Epicor Kinetic Applications.
@REM It iterates through subfolders in the "layers" and "Apps" directories, identifies the latest version of zip files,
@REM and creates new zip files if changes are detected.

@REM The script performs the following steps:
@REM 1. Sets up the environment and checks for the existence of a temporary directory.
@REM 2. Defines the base folder and initializes variables for version control.
@REM 3. Loops through each subfolder in the "layers" and "Apps" directories.
@REM 4. For each subfolder, it identifies the latest version of zip files.
@REM 5. Compares the contents of the current subfolder with the previous version.
@REM 6. If changes are detected, it creates a new zip file with an incremented version number.
@REM 7. Cleans up temporary files and directories created during the process.

@REM The script uses PowerShell commands for zip file operations and WMIC for accurate timestamp comparison.

@echo off
setlocal enabledelayedexpansion

if exist !SystemDrive!\Temp ( 
	set "TEMP=!SYSTEMDRIVE!\Temp"
)

REM Define the base folder and file name
set "baseFolder=%CD%"
set "layerName="

REM Loop through each subfolder in the base folder(s)
for /D %%D in ("layers\*","Apps\*") do (
    REM Reset the file counter and incremental value for each subfolder
    set "MajorVersion=0"
    set "Revision=0"
    set "layerPath=%%~dpD"
	set "layerFolder=%%~nxD"
    REM for %%i in ("!layerPath!") do set "layerType=%%~nxi"
	for %%i in ("!layerPath:~0,-1!") do set "layerType=%%~nxi"
	echo !layerType! !layerFolder! !layerPath!
	echo *******************************************************
    for /f "tokens=3 delims=." %%a in ("%%~nxD") do (
        set "layerName=%%a"

        REM Find the largest file number in the current subfolder
        for %%F in ("!layerName!_*.zip") do (
            for /F "tokens=2 delims=_" %%V in ("%%~nF") do (
                if %%V gtr !MajorVersion! (
                    set "MajorVersion=%%V"
                    set /A "PrevRevision=!Revision!"
                    set "Revision=0"
                ) else if %%V equ !MajorVersion! (
                    set "PrevVersion=!MajorVersion!"
                    REM for /R ".\" %%G in ("!layerName!_!MajorVersion!_*.zip") do (
                    for /R ".\" %%G in ("!layerName!_!MajorVersion!_*.zip") do (
                        for /F "tokens=3 delims=_" %%R in ("%%~nG") do (
                            if %%R geq !Revision! (
                                set /A "PrevRevision=%%R"
                                set /A "Revision=%%R + 1"
                            )
							REM echo .
							REM echo fileIteration: %%F %%G Version: !MajorVersion!  verIteration: %%V  nextRev: !Revision!  revIteration: %%R
                        )
						REM echo fileIteration: %%F %%G Version: !MajorVersion!  verIteration: %%V  nextRev: !Revision!
                    )
                )
            )
		)

		REM Create the previous and new file name
		set newFileName=!layerName!_!MajorVersion!_!Revision!
		if defined PrevVersion if defined PrevRevision (
			set "prevFileName=!layerName!_!PrevVersion!_!PrevRevision!"
		)
		echo newFileName: !newFileName! PrevFileName: !prevFileName! Version: !MajorVersion! Revision: !Revision!

		if not exist "!newFileName!.zip" (
			REM check for existing newFileName before proceeding
			
			if exist "!prevFileName!.zip" (
				set "prevTempDir=!TEMP!\!prevFileName!"

				echo .
				echo Create a temporary directory for comparison !prevTempDir!
				mkdir "!prevTempDir!" 2>nul
				
				REM Extract the existing zip file to the temporary directory
				powershell -Command "Expand-Archive -Path '!prevFileName!.zip' -DestinationPath '!prevTempDir!' -Force"
				
				REM Compare the contents of the folder with the temporary directory
				set "changed=0"
				echo ---------------------------------------------------
				echo %%D
				pushd %CD%\%%D
				for /R %%G in (*) do (
					set "fullPath=%%G"
					set "relativePath=!fullPath:%baseFolder%=!"
					if "!relative_path:~0,1!"== "\" set "relative_path=!relative_path:~1!"
					echo debug -- fileIteration: %%G  !currentFileDate! compareLocation: !fileInTemp!  !tempFileDate!  relativePath=!relativePath!
					set "fileInTemp=!prevTempDir!\!relativePath!"
					if not exist "!fileInTemp!" (
						set "changed=1"
						echo File %%G is new or modified. tempFile: !fileInTemp!
					) else (
						REM Compare modification timestamps using WMIC for accurate comparison
						for %%H in ("!fileInTemp!") do (
							set currentFileDate=%%~tG
							set tempFileDate=%%~tH
							if not defined currentFileDate (
								set "changed=1"
								echo no currentFileDate obtained for %%G
							) else if not defined tempFileDate (
								set "changed=1"
								echo no datestamp obtained for %%H
							) else if !currentFileDate! NEQ !tempFileDate! (
								set "changed=1"
								echo File %%~nxG is modified.
							)
							REM echo fileIteration: %%G  !currentFileDate! compareLocation: !fileInTemp!  prevFileDate: !tempFileDate!
						)
					)
				)
				popd
				if !changed! equ 0 ( echo No changes detected in %%D  )
				echo ---------------------------------------------------
				echo Clean up the temporary directory !prevTempDir!
				rmdir /s /q "!prevTempDir!"
			
			) else ( set "changed=1" )


			REM If there are changes, create the new zip file
			if !changed! equ 1 (
				set "tempDir=!TEMP!\!newFileName!" 
				echo Creating temp directory for Structure Archival !tempDir!\
				
				mkdir !tempDir!\!layerType!\!layerFolder!
				
				echo Copying !CD!\!layerType!\!layerFolder! to !tempDir!\!layerType!\
				echo xcopy /e !CD!\!layerType!\!layerFolder! !tempDir!\!layerType!\!layerFolder! /Q
				xcopy /e "!CD!\!layerType!\!layerFolder!" "!tempDir!\!layerType!\!layerFolder!" /Q

				echo Creating new zip file: !newFileName!.zip
				PowerShell -Command "Compress-Archive -Path '!Temp!\!newFileName!\*' -DestinationPath '!CD!\!newFileName!.zip' -Update -CompressionLevel Optimal"
				
				REM echo "Execution paused for debugging, !tempDir!\ contains proposed content for !CD!\!newFileName!.zip"
				REM echo "press <Ctrl+C> to abort and save or <Enter> to delete"
				REM pause
				
				echo cleaning up temp files !tempDir!
				rmdir /s /q !tempDir!
			) else (
				echo No changes detected in %%D, skipping...
			)


		) else (
			echo Zip file !newFileName!.zip already exists, skipping...
		)
	)
)
endlocal

And for @Mark_Wonsil here it is converted to a few other languages. Including French :smiley: and of course PowerShell. Untested but I was surprised at how quick my sweatbox development team managed to punch it out… Sadly they don’t do Rockstar or some other languages like ALGOL.
It wanted to send this image bac.
image
But all I got was.

Pretty disappointed, wouldn’t even to Softbridge Basic… Now there’s a blast from the past.

Enjoy the file folks
BuildKinetApplicationInDifferentLanguages.zip (40.9 KB)

By the way this was just a bit of fun and was not intended to demean @Kevin_Barrow efforts in any way. If it comes over that way, am sorry and will take this post down just let me know. :innocent:

I encourage contributions and using everyone on EpiUsers as a sounding board for your thoughts and ideas, and heck, “If it works for you” then what does it matter right?

2 Likes

I actually have met Kevin, and I think you did too :rofl:

2 Likes

… apparently, my wordsmithing failed. I intended, “If you use it, you do so at your own risk.”

As I mentioned above, that was my intent.

The batch file is intended to create new zip files when changes are detected to any of the JSON files from any apps in the directory structure.

Yes, you can do that manually; I found it easier to automate the process.

Yes, generally speaking, you could PowerShell everything; however, I could not do so in my environment because of a group policy restriction. Albeit I do not remember what ATM.

I chose a batch file with a bit of PowerShell because that is available in my environment. Python, Cobol, c#, etc, all require interpreters to be installed that are not available in the default install in our environment.

No offense taken.

Great, I’m glad it sparked additional ideas, and I look forward to seeing them should you get around to building them.

Best regards,

Kevin

5 Likes

Added Rust as well :stuck_out_tongue_winking_eye:

2 Likes

The bat file above had a few issues, and I decided to pivot based in part on some of the user feedback here.

The bat file forced users to build all or none of the apps.
It did do some revision checking, but it was slow doing so,
It could take a long time to build the specific application you wanted.
There are some parsing issues if your application has ‘_’ and those are just the ones I found before shifting gears.

The Python script below is a lot similar, more modular in design, and could be integrated into other work flows

There is still room for improvement but it’s 10x better than the bat file.


The Python script below:

#!/usr/bin/env python3
"""
build_kinetic_app.py

USAGE:
  python build_kinetic_app.py <folder1> [folder2] ...

EXAMPLES:
  python build_kinetic_app.py MyApp
  python build_kinetic_app.py Apps/MyApp AnotherApp

DESCRIPTION:
  • For each passed folder, ensures it starts with "Apps/".  
  • Recursively finds every *.json or *.jsonc file.  
  • Preserves the entire "Apps\..." path inside the ZIP archive.  
  • Creates a timestamped .zip in bin\ with a name like "MyApp_2025-01-28-135454.zip".  
  • Skips any non-existent folder.  

REQUIREMENTS:
  • Python 3.x  
  • Standard library "zipfile", "os", and "datetime".  

NOTES:  
  • If you have no *.json/*.jsonc files, the resulting .zip could be empty.  
  • Adjust as needed (e.g. to include more file types, or to handle additional logic).  
"""

import os
import sys
import zipfile
from datetime import datetime

def main():
    if len(sys.argv) < 2:
        print("Usage: python build_kinetic_app.py <AppFolderOrPath> [...]")
        sys.exit(1)

    # Ensure bin\ directory exists
    os.makedirs("bin", exist_ok=True)

    # Base "Apps" folder in absolute form
    apps_base = os.path.abspath("Apps")  # e.g., "C:/Path/To/Project/Apps"

    for raw_path in sys.argv[1:]:
        # Normalize so that it starts with "Apps/" if not already
        if not raw_path.strip().lower().startswith("apps"):
            source_path = os.path.join("Apps", raw_path)
        else:
            source_path = raw_path

        source_folder = os.path.abspath(source_path)

        if not os.path.isdir(source_folder):
            print(f"[WARNING] Folder not found, skipping: {source_folder}")
            continue

        # Example: "Ice.UIDbd.ProductionTracker"
        app_base_name = os.path.basename(os.path.normpath(source_folder))

        # Build a timestamped ZIP name in bin\
        timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
        zip_filename = f"{app_base_name}_{timestamp}.zip"
        zip_path = os.path.join("bin", zip_filename)

        print()
        print("-------------------------------------------------------------")
        print(f"Building '{source_folder}' => '{zip_path}' (only *.json, *.jsonc)")
        print("-------------------------------------------------------------")

        # Open or create the .zip file fresh
        with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
            # Walk everything under the specified folder
            for root, dirs, files in os.walk(source_folder):
                for fname in files:
                    # Include *.json or *.jsonc
                    lower_name = fname.lower()
                    if lower_name.endswith(".json") or lower_name.endswith(".jsonc"):
                        full_path = os.path.join(root, fname)
                        # Preserve path starting at the folder ABOVE "Apps" so that
                        # inside the ZIP we see: Apps\AppFolder\sub\file.jsonc
                        arcname = os.path.relpath(full_path, start=os.path.dirname(apps_base))
                        print(f"  + {arcname}")
                        zf.write(full_path, arcname=arcname)

        print(f"Created: {zip_path}")

    print("\nAll requested folders have been processed.")

if __name__ == "__main__":
    main()
2 Likes