Просмотр исходного кода

Simpler build, less dependencies (#827)

* Simpler build, less dependencies
* Follow ugly python linter
* Introduce Brewfile & Update Readme
* Make dist.sh target-specific
* Tidy up make output
* Get rid of cat and truncate (I still love cats tho)
* Suppress dd output
* Long live the cat
Anna Prosvetova 4 лет назад
Родитель
Сommit
4303945748
15 измененных файлов с 261 добавлено и 186 удалено
  1. 4 94
      .github/workflows/build.yml
  2. 4 0
      .gitignore
  3. 6 0
      Brewfile
  4. 21 17
      Makefile
  5. 47 43
      ReadMe.md
  6. 1 1
      bootloader/Makefile
  7. 0 6
      docker/Dockerfile
  8. 1 1
      firmware/Makefile
  9. 2 0
      make/defaults.mk
  10. 1 1
      make/git.mk
  11. 9 7
      make/rules.mk
  12. 70 0
      scripts/bin2dfu.py
  13. 52 0
      scripts/dist.sh
  14. 1 1
      scripts/flipper/app.py
  15. 42 15
      scripts/meta.py

+ 4 - 94
.github/workflows/build.yml

@@ -67,66 +67,19 @@ jobs:
           fi
           
           echo "WORKFLOW_BRANCH_OR_TAG=${BRANCH_OR_TAG}" >> $GITHUB_ENV
+          echo "DIST_SUFFIX=${SUFFIX}" >> $GITHUB_ENV
           echo "::set-output name=artifacts-path::${BRANCH_OR_TAG}"
           echo "::set-output name=suffix::${SUFFIX}"
           echo "::set-output name=short-hash::${SHA}"
           echo "::set-output name=default-target::${DEFAULT_TARGET}"
 
-      - name: 'Build bootloader in docker'
+      - name: 'Build the firmware in docker'
         uses: ./.github/actions/docker
         with:
           run: |
             for TARGET in ${TARGETS}
             do
-              make -j$(nproc) -C bootloader TARGET=${TARGET}
-            done
-
-      - name: 'Build firmware in docker'
-        uses: ./.github/actions/docker
-        with:
-          run: |
-            for TARGET in ${TARGETS}
-            do
-              make -j$(nproc) -C firmware TARGET=${TARGET}
-            done
-
-      - name: 'Generate full hex file'
-        if: ${{ !github.event.pull_request.head.repo.fork }}
-        uses: ./.github/actions/docker
-        with:
-          run: |
-            for TARGET in ${TARGETS}
-            do
-              srec_cat \
-                bootloader/.obj/${TARGET}/bootloader.hex -Intel \
-                firmware/.obj/${TARGET}/firmware.hex -Intel \
-                -o firmware/.obj/${TARGET}/full.hex -Intel
-            done
-
-      - name: 'Generate full dfu file'
-        if: ${{ !github.event.pull_request.head.repo.fork }}
-        uses: ./.github/actions/docker
-        with:
-          run: |
-            for TARGET in ${TARGETS}
-            do
-              hex2dfu \
-                -i firmware/.obj/${TARGET}/full.hex \
-                -o artifacts/flipper-z-${TARGET}-full-${{steps.names.outputs.suffix}}.dfu \
-                -l "Flipper Zero $(echo $TARGET | tr a-z A-Z)"
-            done
-
-      - name: 'Generate full json file'
-        if: ${{ !github.event.pull_request.head.repo.fork }}
-        uses: ./.github/actions/docker
-        with:
-          run: |
-            for TARGET in ${TARGETS}
-            do
-              jq -s '.[0] * .[1]' \
-                bootloader/.obj/${TARGET}/bootloader.json \
-                firmware/.obj/${TARGET}/firmware.json  \
-                > artifacts/flipper-z-${TARGET}-full-${{steps.names.outputs.suffix}}.json
+              make TARGET=${TARGET}
             done
 
       - name: 'Move upload files'
@@ -136,52 +89,9 @@ jobs:
           run: |
             for TARGET in ${TARGETS}
             do
-              mv bootloader/.obj/${TARGET}/bootloader.dfu \
-                artifacts/flipper-z-${TARGET}-bootloader-${{steps.names.outputs.suffix}}.dfu
-              mv bootloader/.obj/${TARGET}/bootloader.bin \
-                artifacts/flipper-z-${TARGET}-bootloader-${{steps.names.outputs.suffix}}.bin
-              mv bootloader/.obj/${TARGET}/bootloader.elf \
-                artifacts/flipper-z-${TARGET}-bootloader-${{steps.names.outputs.suffix}}.elf
-              mv bootloader/.obj/${TARGET}/bootloader.json \
-                artifacts/flipper-z-${TARGET}-bootloader-${{steps.names.outputs.suffix}}.json
-              mv firmware/.obj/${TARGET}/firmware.dfu \
-                artifacts/flipper-z-${TARGET}-firmware-${{steps.names.outputs.suffix}}.dfu
-              mv firmware/.obj/${TARGET}/firmware.bin \
-                artifacts/flipper-z-${TARGET}-firmware-${{steps.names.outputs.suffix}}.bin
-              mv firmware/.obj/${TARGET}/firmware.elf \
-                artifacts/flipper-z-${TARGET}-firmware-${{steps.names.outputs.suffix}}.elf
-              mv firmware/.obj/${TARGET}/firmware.json \
-                artifacts/flipper-z-${TARGET}-firmware-${{steps.names.outputs.suffix}}.json
+              mv dist/${TARGET}/* artifacts/
             done
 
-      - name: 'Full flash asssembly: bootloader as base'
-        if: ${{ !github.event.pull_request.head.repo.fork }}
-        run: |
-          for TARGET in ${TARGETS}
-          do
-            cp \
-              artifacts/flipper-z-${TARGET}-bootloader-${{steps.names.outputs.suffix}}.bin \
-              artifacts/flipper-z-${TARGET}-full-${{steps.names.outputs.suffix}}.bin
-          done
-
-      - name: 'Full flash asssembly: bootloader padding'
-        if: ${{ !github.event.pull_request.head.repo.fork }}
-        run: |
-          for TARGET in ${TARGETS}
-          do
-            truncate -s 32768 artifacts/flipper-z-${TARGET}-full-${{steps.names.outputs.suffix}}.bin
-          done
-
-      - name: 'Full flash asssembly: append firmware'
-        if: ${{ !github.event.pull_request.head.repo.fork }}
-        run: |
-          for TARGET in ${TARGETS}
-          do
-            cat \
-              artifacts/flipper-z-${TARGET}-firmware-${{steps.names.outputs.suffix}}.bin \
-              >> artifacts/flipper-z-${TARGET}-full-${{steps.names.outputs.suffix}}.bin
-          done
-
       - name: 'Bundle core2 firmware'
         if: ${{ !github.event.pull_request.head.repo.fork }}
         run: |

+ 4 - 0
.gitignore

@@ -24,6 +24,7 @@ __pycache__/
 bindings/
 .DS_Store
 .mxproject
+Brewfile.lock.json
 
 # Visual Studio Code
 .vscode/
@@ -31,3 +32,6 @@ bindings/
 # legendary cmake's
 build
 CMakeLists.txt
+
+# bundle output
+dist

+ 6 - 0
Brewfile

@@ -0,0 +1,6 @@
+cask "gcc-arm-embedded"
+brew "protobuf"
+brew "heatshrink"
+brew "open-ocd"
+brew "clang-format"
+brew "dfu-util"

+ 21 - 17
Makefile

@@ -10,65 +10,69 @@ else ifeq ($(OS), Darwin)
 NPROCS := $(shell sysctl -n hw.ncpu)
 endif
 
+include	$(PROJECT_ROOT)/make/defaults.mk
+
 .PHONY: all
 all: bootloader_all firmware_all
+	@$(PROJECT_ROOT)/scripts/dist.sh
 
 .PHONY: whole
 whole: flash_radio bootloader_flash firmware_flash
 
 .PHONY: clean
 clean: bootloader_clean firmware_clean
+	@rm -rf $(PROJECT_ROOT)/dist/$(TARGET)
 
 .PHONY: flash
 flash: bootloader_flash firmware_flash
 
 .PHONY: debug
 debug:
-	$(MAKE) -C firmware -j$(NPROCS) debug
+	@$(MAKE) -C firmware -j$(NPROCS) debug
 
 .PHONY: blackmagic
 blackmagic:
-	$(MAKE) -C firmware -j$(NPROCS) blackmagic
+	@$(MAKE) -C firmware -j$(NPROCS) blackmagic
 
 .PHONY: wipe
 wipe:
-	$(PROJECT_ROOT)/scripts/flash.py wipe
-	$(PROJECT_ROOT)/scripts/ob.py set
+	@$(PROJECT_ROOT)/scripts/flash.py wipe
+	@$(PROJECT_ROOT)/scripts/ob.py set
 
 .PHONY: bootloader_all
 bootloader_all:
-	$(MAKE) -C $(PROJECT_ROOT)/bootloader -j$(NPROCS) all
+	@$(MAKE) -C $(PROJECT_ROOT)/bootloader -j$(NPROCS) all
 
 .PHONY: firmware_all
 firmware_all:
-	$(MAKE) -C $(PROJECT_ROOT)/firmware -j$(NPROCS) all
+	@$(MAKE) -C $(PROJECT_ROOT)/firmware -j$(NPROCS) all
 
 .PHONY: bootloader_clean
 bootloader_clean:
-	$(MAKE) -C $(PROJECT_ROOT)/bootloader -j$(NPROCS) clean
+	@$(MAKE) -C $(PROJECT_ROOT)/bootloader -j$(NPROCS) clean
 
 .PHONY: firmware_clean
 firmware_clean:
-	$(MAKE) -C $(PROJECT_ROOT)/firmware -j$(NPROCS) clean
+	@$(MAKE) -C $(PROJECT_ROOT)/firmware -j$(NPROCS) clean
 
 .PHONY: bootloader_flash
 bootloader_flash:
 ifeq ($(FORCE), 1)
-	rm $(PROJECT_ROOT)/bootloader/.obj/f*/flash || true
+	@rm $(PROJECT_ROOT)/bootloader/.obj/f*/flash || true
 endif
-	$(MAKE) -C $(PROJECT_ROOT)/bootloader -j$(NPROCS) flash
+	@$(MAKE) -C $(PROJECT_ROOT)/bootloader -j$(NPROCS) flash
 
 .PHONY: firmware_flash
 firmware_flash:
 ifeq ($(FORCE), 1)
-	rm $(PROJECT_ROOT)/firmware/.obj/f*/flash || true
+	@rm $(PROJECT_ROOT)/firmware/.obj/f*/flash || true
 endif
-	$(MAKE) -C $(PROJECT_ROOT)/firmware -j$(NPROCS) flash
+	@$(MAKE) -C $(PROJECT_ROOT)/firmware -j$(NPROCS) flash
 
 .PHONY: flash_radio
 flash_radio:
-	$(PROJECT_ROOT)/scripts/flash.py core2radio 0x080CA000 $(COPRO_DIR)/stm32wb5x_BLE_Stack_full_fw.bin
-	$(PROJECT_ROOT)/scripts/ob.py set
+	@$(PROJECT_ROOT)/scripts/flash.py core2radio 0x080CA000 $(COPRO_DIR)/stm32wb5x_BLE_Stack_full_fw.bin
+	@$(PROJECT_ROOT)/scripts/ob.py set
 
 .PHONY: flash_radio_fus
 flash_radio_fus:
@@ -83,9 +87,9 @@ flash_radio_fus:
 
 .PHONY: flash_radio_fus_please_i_m_not_going_to_complain
 flash_radio_fus_please_i_m_not_going_to_complain:
-	$(PROJECT_ROOT)/scripts/flash.py core2fus 0x080EC000 --statement=AGREE_TO_LOOSE_FLIPPER_FEATURES_THAT_USES_CRYPTO_ENCLAVE $(COPRO_DIR)/stm32wb5x_FUS_fw_for_fus_0_5_3.bin
-	$(PROJECT_ROOT)/scripts/flash.py core2fus 0x080EC000 --statement=AGREE_TO_LOOSE_FLIPPER_FEATURES_THAT_USES_CRYPTO_ENCLAVE $(COPRO_DIR)/stm32wb5x_FUS_fw.bin
-	$(PROJECT_ROOT)/scripts/ob.py set
+	@$(PROJECT_ROOT)/scripts/flash.py core2fus 0x080EC000 --statement=AGREE_TO_LOOSE_FLIPPER_FEATURES_THAT_USES_CRYPTO_ENCLAVE $(COPRO_DIR)/stm32wb5x_FUS_fw_for_fus_0_5_3.bin
+	@$(PROJECT_ROOT)/scripts/flash.py core2fus 0x080EC000 --statement=AGREE_TO_LOOSE_FLIPPER_FEATURES_THAT_USES_CRYPTO_ENCLAVE $(COPRO_DIR)/stm32wb5x_FUS_fw.bin
+	@$(PROJECT_ROOT)/scripts/ob.py set
 
 FORMAT_SOURCES = $(shell find applications bootloader core -iname "*.h" -o -iname "*.c" -o -iname "*.cpp")
 

+ 47 - 43
ReadMe.md

@@ -55,17 +55,12 @@ One liner: `./flash_core1_main.sh`
 
 3. Run `dfu-util -D full.dfu -a 0`
 
-# Build from source
+# Build with Docker
 
 ## Prerequisites
 
 1. Install [Docker Engine and Docker Compose](https://www.docker.com/get-started)
-2. Clone the repo:
-   ```sh
-   git clone https://github.com/flipperdevices/flipperzero-firmware
-   cd flipperzero-firmware
-   ```
-3. Prepare the container:
+2. Prepare the container:
    ```sh
    docker-compose up -d
    ```
@@ -73,61 +68,70 @@ One liner: `./flash_core1_main.sh`
 ## Compile everything
 
 ```sh
-docker-compose exec dev make -j$(nproc)
+docker-compose exec dev make
 ```
 
-## Flash everything
+Check `dist/` for build outputs.
+
+Use **`flipper-z-{target}-full-{suffix}.dfu`** to flash your device.
+
+# Build on Linux/macOS
+
+## macOS Prerequisites
 
+Make sure you have [brew](https://brew.sh) and install all the dependencies:
 ```sh
-docker-compose exec dev make -j$(nproc) whole
+brew bundle --verbose
 ```
 
-## Compile bootloader
+## Linux Prerequisites
+
+### gcc-arm-none-eabi
 
 ```sh
-docker-compose exec dev make -j$(nproc) -C bootloader
+toolchain="gcc-arm-none-eabi-10.3-2021.10"
+toolchain_package="$toolchain-$(uname -m)-linux"
+
+wget -P /opt "https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.10/$toolchain_package.tar.bz2"
+
+tar xjf /opt/$toolchain_package.tar.bz2 -C /opt
+rm /opt/$toolchain_package.tar.bz2
+
+for file in /opt/$toolchain/bin/* ; do ln -s "${file}" "/usr/bin/$(basename ${file})" ; done
 ```
 
-Bootloader compilation results:
-* `bootloader/.obj/f7/bootloader.elf`
-* `bootloader/.obj/f7/bootloader.hex`
-* `bootloader/.obj/f7/bootloader.bin`
-* **`bootloader/.obj/f7/bootloader.dfu`** - should be used to flash
+### Optional dependencies
 
-## Compile firmware
+- openocd (debugging/flashing over SWD)
+- heatshrink (compiling image assets)
+- clang-format (code formatting)
+- dfu-util (flashing over USB DFU)
+- protobuf (compiling proto sources)
 
+For example, to install them on Debian, use:
 ```sh
-docker-compose exec dev make -j$(nproc) -C firmware
+apt update
+apt install openocd clang-format-13 dfu-util protobuf-compiler
 ```
 
-Firmware compilation results:
-* `firmware/.obj/f7/firmware.elf`
-* `firmware/.obj/f7/firmware.hex`
-* `firmware/.obj/f7/firmware.bin`
-* **`firmware/.obj/f7/firmware.dfu`** - should be used to flash
+heatshrink has to be compiled [from sources](https://github.com/atomicobject/heatshrink).
 
-## Concatenate bootloader and firmware
+## Compile everything
 
-You might want to do this to distribute the firmware as a single file.
+```sh
+make
+```
 
-That's exactly how we generate our `full` builds.
+Check `dist/` for build outputs.
 
-1. Concatenate HEX files:
-   ```sh
-   docker-compose exec dev srec_cat \
-    bootloader/.obj/f7/bootloader.hex -Intel \
-    firmware/.obj/f7/firmware.hex -Intel \
-    -o firmware/.obj/f7/full.hex -Intel
-   ```
-2. Convert HEX to DFU:
-   ```sh
-   docker-compose exec dev hex2dfu \
-    -i firmware/.obj/f7/full.hex \
-    -o firmware/.obj/f7/full.dfu \
-    -l "Flipper Zero F7"
-   ```
+Use **`flipper-z-{target}-full-{suffix}.dfu`** to flash your device.
+
+## Flash everything
 
-Finally, you will have **`firmware/.obj/f7/full.dfu`** file that can be distributed and flashed.
+Connect your device via ST-Link and run:
+```sh
+make whole
+```
 
 # Links
 * Discord: [flipp.dev/discord](https://flipp.dev/discord)
@@ -198,4 +202,4 @@ Finally, you will have **`firmware/.obj/f7/full.dfu`** file that can be distribu
   * toolbox - toolbox of things that we are using but don't place in core
   * u8g2 - graphics library that we use to draw GUI
 - make - make helpers
-- scripts - supplimentary scripts
+- scripts - supplementary scripts

+ 1 - 1
bootloader/Makefile

@@ -8,7 +8,7 @@ ASM_SOURCES		+= $(wildcard src/*.s)
 C_SOURCES		+= $(wildcard src/*.c)
 CPP_SOURCES		+= $(wildcard src/*.cpp)
 
-TARGET			?= f7
+include			$(PROJECT_ROOT)/make/defaults.mk
 TARGET_DIR		= targets/$(TARGET)
 include			$(TARGET_DIR)/target.mk
 

+ 0 - 6
docker/Dockerfile

@@ -8,14 +8,12 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-instal
         clang-format-12 \
         dfu-util \
         openocd \
-        srecord \
         libncurses5 \
         python-setuptools \
         libpython2.7-dev \
         libxml2-dev \
         libxslt1-dev \
         zlib1g-dev \
-        jq \
         wget && \
     apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
 
@@ -30,10 +28,6 @@ RUN wget --progress=dot:giga "https://developer.arm.com/-/media/Files/downloads/
 RUN wget --progress=dot:giga -O - https://bootstrap.pypa.io/pip/2.7/get-pip.py | python2 && \
     pip install --no-cache-dir lxml==4.6.3
 
-RUN git clone https://github.com/rusdacent/hex2dfu.git && \
-    cd hex2dfu && gcc hex2dfu.c ED25519/*.c -o hex2dfu && mv ./hex2dfu /usr/local/bin/hex2dfu  && \
-    hex2dfu -h
-
 RUN git clone --depth 1 --branch v0.4.1 https://github.com/atomicobject/heatshrink.git && \
     cd heatshrink && make && mv ./heatshrink /usr/local/bin/heatshrink
 

+ 1 - 1
firmware/Makefile

@@ -12,7 +12,7 @@ CFLAGS			+= -I$(PROJECT_ROOT) -Itargets/furi-hal-include
 CFLAGS			+= -Werror -Wno-address-of-packed-member
 CPPFLAGS		+= -Werror
 
-TARGET			?= f7
+include			$(PROJECT_ROOT)/make/defaults.mk
 TARGET_DIR		= targets/$(TARGET)
 include			$(TARGET_DIR)/target.mk
 

+ 2 - 0
make/defaults.mk

@@ -0,0 +1,2 @@
+TARGET	?= f7
+export TARGET

+ 1 - 1
make/git.mk

@@ -2,7 +2,7 @@ GIT_COMMIT		:= $(shell git rev-parse --short HEAD || echo 'unknown')
 GIT_BRANCH		:= $(shell echo $${WORKFLOW_BRANCH_OR_TAG-$$(git rev-parse --abbrev-ref HEAD || echo 'unknown')})
 GIT_BRANCH_NUM	:= $(shell git rev-list --count HEAD || echo 'nan')
 BUILD_DATE		:= $(shell date '+%d-%m-%Y' || echo 'unknown')
-VERSION			:= $(shell git describe --tags --abbrev=0 --exact-match || echo 'unknown')
+VERSION			:= $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null || echo 'unknown')
 
 CFLAGS += \
 	-DGIT_COMMIT=\"$(GIT_COMMIT)\" \

+ 9 - 7
make/rules.mk

@@ -33,6 +33,7 @@ CHECK_AND_REINIT_SUBMODULES_SHELL=\
 $(info $(shell $(CHECK_AND_REINIT_SUBMODULES_SHELL)))
 
 all: $(OBJ_DIR)/$(PROJECT).elf $(OBJ_DIR)/$(PROJECT).hex $(OBJ_DIR)/$(PROJECT).bin $(OBJ_DIR)/$(PROJECT).dfu $(OBJ_DIR)/$(PROJECT).json
+	@:
 
 $(OBJ_DIR)/$(PROJECT).elf: $(OBJECTS)
 	@echo "\tLD\t" $@
@@ -47,27 +48,28 @@ $(OBJ_DIR)/$(PROJECT).bin: $(OBJ_DIR)/$(PROJECT).elf
 	@echo "\tBIN\t" $@
 	@$(BIN) $< $@
 
-$(OBJ_DIR)/$(PROJECT).dfu: $(OBJ_DIR)/$(PROJECT).hex
+$(OBJ_DIR)/$(PROJECT).dfu: $(OBJ_DIR)/$(PROJECT).bin
 	@echo "\tDFU\t" $@
-	@hex2dfu \
-		-i $(OBJ_DIR)/$(PROJECT).hex \
+	@../scripts/bin2dfu.py \
+		-i $(OBJ_DIR)/$(PROJECT).bin \
 		-o $(OBJ_DIR)/$(PROJECT).dfu \
+		-a $(FLASH_ADDRESS) \
 		-l "Flipper Zero $(shell echo $(TARGET) | tr a-z A-Z)" > /dev/null
 
 $(OBJ_DIR)/$(PROJECT).json: $(OBJ_DIR)/$(PROJECT).dfu
 	@echo "\tJSON\t" $@
-	@python3 ../scripts/meta.py -p $(PROJECT) $(CFLAGS) > $(OBJ_DIR)/$(PROJECT).json
+	@../scripts/meta.py generate -p $(PROJECT) $(CFLAGS) > $(OBJ_DIR)/$(PROJECT).json
 
 $(OBJ_DIR)/%.o: %.c $(OBJ_DIR)/BUILD_FLAGS
-	@echo "\tCC\t" $< "->" $@
+	@echo "\tCC\t" $(subst $(PROJECT_ROOT)/,,$(realpath $<)) "->" $@
 	@$(CC) $(CFLAGS) -c $< -o $@
 
 $(OBJ_DIR)/%.o: %.s $(OBJ_DIR)/BUILD_FLAGS
-	@echo "\tASM\t" $< "->" $@
+	@echo "\tASM\t" $(subst $(PROJECT_ROOT)/,,$(realpath $<)) "->" $@
 	@$(AS) $(CFLAGS) -c $< -o $@
 
 $(OBJ_DIR)/%.o: %.cpp $(OBJ_DIR)/BUILD_FLAGS
-	@echo "\tCPP\t" $< "->" $@
+	@echo "\tCPP\t" $(subst $(PROJECT_ROOT)/,,$(realpath $<)) "->" $@
 	@$(CPP) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
 
 $(OBJ_DIR)/flash: $(OBJ_DIR)/$(PROJECT).bin

+ 70 - 0
scripts/bin2dfu.py

@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+
+import os
+import struct
+from zlib import crc32
+
+from flipper.app import App
+
+
+class Main(App):
+    def init(self):
+        self.parser.add_argument("-i", "--input", help=".bin input path", required=True)
+        self.parser.add_argument(
+            "-o", "--output", help=".dfu output path", required=True
+        )
+        self.parser.add_argument(
+            "-a",
+            "--address",
+            help="Flash address",
+            type=lambda x: int(x, 0),
+            required=True,
+        )
+        self.parser.add_argument(
+            "-l", "--label", help="DFU Target label", required=True
+        )
+        self.parser.add_argument(
+            "--vid", help="USB Vendor ID", default=0x0483, type=lambda x: int(x, 0)
+        )
+        self.parser.add_argument(
+            "--pid", help="USB Product ID", default=0xDF11, type=lambda x: int(x, 0)
+        )
+
+        self.parser.set_defaults(func=self.convert)
+
+    def convert(self):
+        if not os.path.exists(self.args.input):
+            self.logger.error(f'"{self.args.input}" does not exist')
+            return 1
+
+        with open(self.args.input, mode="rb") as file:
+            bin = file.read()
+
+        data = struct.pack("<II", self.args.address, len(bin)) + bin
+
+        # Target prefix
+        szTargetName = self.args.label.encode("ascii")
+
+        data = (
+            struct.pack("<6sBI255sII", b"Target", 0, 1, szTargetName, len(data), 1)
+            + data
+        )
+
+        # Prefix
+        data = struct.pack("<5sBIB", b"DfuSe", 0x01, len(data) + 11, 1) + data
+
+        # Suffix
+        data += struct.pack(
+            "<HHHH3sB", 0xFFFF, self.args.pid, self.args.vid, 0x011A, b"UFD", 16
+        )
+
+        dwCRC = ~crc32(data) & 0xFFFFFFFF
+
+        data += struct.pack("<I", dwCRC)
+
+        open(self.args.output, "wb").write(data)
+        return 0
+
+
+if __name__ == "__main__":
+    Main()()

+ 52 - 0
scripts/dist.sh

@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+
+set -e
+
+suffix="${DIST_SUFFIX:=local}"
+
+rm -rf "dist/${TARGET}"
+mkdir -p "dist/${TARGET}"
+
+# copy build outputs
+cp bootloader/.obj/${TARGET}/bootloader.elf \
+    dist/${TARGET}/flipper-z-${TARGET}-bootloader-${suffix}.elf
+cp bootloader/.obj/${TARGET}/bootloader.bin \
+    dist/${TARGET}/flipper-z-${TARGET}-bootloader-${suffix}.bin
+cp bootloader/.obj/${TARGET}/bootloader.dfu \
+    dist/${TARGET}/flipper-z-${TARGET}-bootloader-${suffix}.dfu
+cp bootloader/.obj/${TARGET}/bootloader.json \
+    dist/${TARGET}/flipper-z-${TARGET}-bootloader-${suffix}.json
+cp firmware/.obj/${TARGET}/firmware.elf \
+    dist/${TARGET}/flipper-z-${TARGET}-firmware-${suffix}.elf
+cp firmware/.obj/${TARGET}/firmware.bin \
+    dist/${TARGET}/flipper-z-${TARGET}-firmware-${suffix}.bin
+cp firmware/.obj/${TARGET}/firmware.dfu \
+    dist/${TARGET}/flipper-z-${TARGET}-firmware-${suffix}.dfu
+cp firmware/.obj/${TARGET}/firmware.json \
+    dist/${TARGET}/flipper-z-${TARGET}-firmware-${suffix}.json
+
+# generate full.bin
+cp dist/${TARGET}/flipper-z-${TARGET}-bootloader-${suffix}.bin \
+    dist/${TARGET}/flipper-z-${TARGET}-full-${suffix}.bin
+dd if=/dev/null of=dist/${TARGET}/flipper-z-${TARGET}-full-${suffix}.bin bs=1 count=0 seek=32768 2> /dev/null
+cat dist/${TARGET}/flipper-z-${TARGET}-firmware-${suffix}.bin \
+    >>dist/${TARGET}/flipper-z-${TARGET}-full-${suffix}.bin \
+    2> /dev/null
+
+# generate full.dfu
+./scripts/bin2dfu.py \
+    -i dist/${TARGET}/flipper-z-${TARGET}-full-${suffix}.bin \
+    -o dist/${TARGET}/flipper-z-${TARGET}-full-${suffix}.dfu \
+    -a 0x08000000 \
+    -l "Flipper Zero $(echo ${TARGET} | tr a-z A-Z)"
+
+# generate full.json
+./scripts/meta.py merge \
+    -i dist/${TARGET}/flipper-z-${TARGET}-bootloader-${suffix}.json \
+    dist/${TARGET}/flipper-z-${TARGET}-firmware-${suffix}.json \
+    >dist/${TARGET}/flipper-z-${TARGET}-full-${suffix}.json
+
+echo "Firmware binaries can be found at:"
+echo -e "\t$(pwd)/dist/${TARGET}"
+echo "Use this file to flash your Flipper:"
+echo -e "\tflipper-z-${TARGET}-full-${suffix}.dfu"

+ 1 - 1
scripts/flipper/app.py

@@ -15,7 +15,7 @@ class App:
         self.init()
 
     def __call__(self):
-        self.args = self.parser.parse_args()
+        self.args, _ = self.parser.parse_known_args()
         if "func" not in self.args:
             self.parser.error("Choose something to do")
         # configure log output

+ 42 - 15
scripts/meta.py

@@ -1,31 +1,58 @@
 #!/usr/bin/env python3
 
-import argparse
+from flipper.app import App
 import json
 
 
-class Main:
-    def __init__(self):
-        # parse CFLAGS
-        self.parser = argparse.ArgumentParser(allow_abbrev=False)
-        self.parser.add_argument("-p", dest="project", required=True)
-        self.parser.add_argument("-DBUILD_DATE", dest="build_date", required=True)
-        self.parser.add_argument("-DGIT_COMMIT", dest="commit", required=True)
-        self.parser.add_argument("-DGIT_BRANCH", dest="branch", required=True)
-        self.parser.add_argument("-DTARGET", dest="target", type=int, required=True)
-
-    def __call__(self):
-        self.args, _ = self.parser.parse_known_args()
-
+class Main(App):
+    def init(self):
+        self.subparsers = self.parser.add_subparsers(help="sub-command help")
+
+        # generate
+        self.parser_generate = self.subparsers.add_parser(
+            "generate", help="Generate JSON meta file"
+        )
+        self.parser_generate.add_argument("-p", dest="project", required=True)
+        self.parser_generate.add_argument(
+            "-DBUILD_DATE", dest="build_date", required=True
+        )
+        self.parser_generate.add_argument("-DGIT_COMMIT", dest="commit", required=True)
+        self.parser_generate.add_argument("-DGIT_BRANCH", dest="branch", required=True)
+        self.parser_generate.add_argument(
+            "-DTARGET", dest="target", type=int, required=True
+        )
+        self.parser_generate.set_defaults(func=self.generate)
+
+        # merge
+        self.parser_merge = self.subparsers.add_parser(
+            "merge", help="Merge JSON meta files"
+        )
+        self.parser_merge.add_argument(
+            "-i", dest="input", action="append", nargs="+", required=True
+        )
+        self.parser_merge.set_defaults(func=self.merge)
+
+    def generate(self):
         meta = {}
         for k, v in vars(self.args).items():
-            if k == "project":
+            if k == "project" or k == "func":
                 continue
             if isinstance(v, str):
                 v = v.strip('"')
             meta[self.args.project + "_" + k] = v
 
         print(json.dumps(meta, indent=4))
+        return 0
+
+    def merge(self):
+        full = {}
+        for path in self.args.input[0]:
+            with open(path, mode="r") as file:
+                dict = json.loads(file.read())
+                full |= dict
+
+        print(json.dumps(full, indent=4))
+        return 0
 
 
 if __name__ == "__main__":