Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-deb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
name: "Ubuntu 22.04, Shared: ${{ matrix.shared }}"
strategy:
matrix:
shared: ["True"]
shared: ["False"]

steps:
- name: Install dependencies
Expand Down
79 changes: 79 additions & 0 deletions .github/workflows/build-ubuntu-22.04.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Build, Test, and Draft Releae for static and dynamic versions of the
# MTConnect Agent on Windows, Mac OS, and Linux.
#
# The Windows x86 and x86_64 builds create ZIP packages and attach them to
# a draft release when the commit is tagged.
#
# Secret required for Release:
# RELEASE_GITHUB_TOKEN - Release token created by admin in Settings / Developer Settings / Personal access tokens /
# Find-grained tokens
# The token must be renewed every 90 days.

name: Build MTConnect C++ Agent for Ubuntu

on:
# Enable automated build once verified
pull_request:
paths-ignore: ["**/*.md", "LICENSE.txt", ".gitignore"]
branches: [ "main", "main-dev" ]

push:
paths-ignore: ["**/*.md", "LICENSE.txt", ".gitignore"]
branches: [ "main", "main-dev", "jarsenault-debify" ]
tags:
- "v*.*.*"

# Allow manually run workflows
workflow_dispatch:

jobs:
build_linux:
runs-on: ubuntu-22.04
name: "Ubuntu Latest, Shared: ${{ matrix.shared }}"
strategy:
matrix:
shared: ["False"]

steps:
- name: Install dependencies
shell: bash
run: |
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt update
sudo apt install -y build-essential cmake gcc-13 g++-13 python3 python3-pip autoconf automake rake ruby

- name: Checkout Agent
uses: actions/checkout@v3

- name: Cache conan packages
id: cache
uses: actions/cache@v3
with:
path: ~/.conan2
key: ${{ runner.os }}-build-${{ matrix.shared }}-${{ hashFiles('**/conanfile.py') }}

- name: Install Conan
uses: turtlebrowser/get-conan@v1.2

- name: Setup Conan
if: steps.cache.outputs.cache-hit != 'true'
run: |
conan profile detect -f

- name: Cleanup Prior Build
if: steps.cache.outputs.cache-hit == 'true'
continue-on-error: true
run: |
conan remove mtconnect_agent -c

- name: Build and Test C++ Agent
shell: bash
env:
LDFLAGS: "-static-libgcc -static-libstdc++"
run: |
export CTEST_OUTPUT_ON_FAILURE=TRUE
conan create . --build=missing -pr conan/profiles/gcc -o shared=${{ matrix.shared }} -o development=True -o cpack=True -o cpack_name=dist -o cpack_destination=${{ github.workspace }} --test-folder=
- name: Build Debian Package
shell: bash
run: |
./make-agent-deb.sh
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ cmake-build-release/**
ACCESS_TOKEN
.DS_Store
.vscode
CMakeUserPresets.json
CMakeUserPresets.json

# Local build artifacts produced by docker/build.sh (matches CI behavior).
*.deb
_CPack_Packages/
pkgroot/DEBIAN/control
pkgroot/home/
2 changes: 1 addition & 1 deletion conan/profiles/docker
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ compiler.libcxx=libstdc++11
[options]
without_ipv6=True

[system_tools]
[platform_tool_requires]
cmake/>3.23.0

2 changes: 1 addition & 1 deletion conan/profiles/gcc
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ include(default)
[settings]
compiler.libcxx=libstdc++11

[system_tools]
[platform_tool_requires]
cmake/>3.23.0
2 changes: 1 addition & 1 deletion conan/profiles/macos
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ include(default)
compiler=apple-clang
compiler.cppstd=gnu17

[system_tools]
[platform_tool_requires]
cmake/>3.26.0


Expand Down
5 changes: 5 additions & 0 deletions conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ class MTConnectAgentConan(ConanFile):
"boost*:without_serialization": True,
"boost*:without_wave": True,
"boost*:without_graph_parallel": True,
# libboost_stacktrace_from_exception interposes on
# __cxa_allocate_exception, which collides with libstdc++.a under
# full static linking. The agent does not use boost::stacktrace,
# so build boost without it.
"boost*:without_stacktrace": True,

"libxml2*:shared": False,
"libxml2*:include_utils": False,
Expand Down
37 changes: 37 additions & 0 deletions docker/Dockerfile.ubuntu-22.04
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Mirrors the environment used by .github/workflows/build-ubuntu-22.04.yml.
# Used by docker/build.sh to run the cppagent build/tests locally inside a
# container that matches CI. Source is expected to be bind-mounted at /work,
# and HOME is overridden at run time to a named volume so the conan cache
# persists across invocations.

FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
software-properties-common \
ca-certificates \
gnupg \
&& add-apt-repository ppa:ubuntu-toolchain-r/test -y \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
cmake \
gcc-13 \
g++-13 \
python3 \
python3-pip \
autoconf \
automake \
rake \
ruby \
git \
sudo \
&& update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 \
&& update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 100 \
&& rm -rf /var/lib/apt/lists/*

RUN pip3 install --no-cache-dir "conan>=2.0,<3.0"

WORKDIR /work
131 changes: 131 additions & 0 deletions docker/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
# Build and test the cppagent inside an ubuntu-22.04 container that mirrors
# the GitHub Actions build (.github/workflows/build-ubuntu-22.04.yml).
#
# Usage:
# docker/build.sh # full conan create, matches CI
# docker/build.sh shell # interactive bash inside the container
# docker/build.sh deb [version] # package dist.tar.gz as mtconnect-agent_<version>_all.deb
# docker/build.sh -- <cmd...> # run an arbitrary command in the container
#
# The conan cache is persisted on the host at ~/.cache/cppagent-conan
# (override with CPPAGENT_CONAN_CACHE) so subsequent builds skip recompiling
# boost et al.

set -euo pipefail

readonly REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly IMAGE_TAG="cppagent-build:ubuntu-22.04"
readonly DOCKERFILE="${REPO_ROOT}/docker/Dockerfile.ubuntu-22.04"
# Host-side conan cache. A host directory (rather than a named docker volume)
# is used so it ends up owned by the host user, letting the container's
# --user $(id -u):$(id -g) actually write to it without a chown step.
readonly CACHE_DIR="${CPPAGENT_CONAN_CACHE:-${HOME}/.cache/cppagent-conan}"

if [[ ! -f "${DOCKERFILE}" ]]; then
echo "error: ${DOCKERFILE} not found" >&2
exit 1
fi

# Build the image only when the Dockerfile is newer than the image, or the
# image doesn't exist yet. `docker build` is itself a no-op when nothing has
# changed, but skipping the call when possible avoids the daemon round-trip.
if ! docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
echo ">>> Building ${IMAGE_TAG}"
docker build -t "${IMAGE_TAG}" -f "${DOCKERFILE}" "${REPO_ROOT}/docker"
fi

# Make sure the conan cache directory exists on the host (will be owned by
# the host user so the container can write to it as that user).
mkdir -p "${CACHE_DIR}"

# The default command runs the same `conan create` invocation as CI. The
# LDFLAGS match the workflow so the produced binaries are statically linked
# against libgcc/libstdc++.
readonly CI_BUILD_CMD='set -euo pipefail
export CTEST_OUTPUT_ON_FAILURE=TRUE
export LDFLAGS="-static-libgcc -static-libstdc++"
conan profile detect -f
conan create . \
--build=missing \
-pr conan/profiles/gcc \
-o shared=False \
-o development=True \
-o cpack=True \
-o cpack_name=dist \
-o cpack_destination=/work \
--test-folder='

# Replicates the GitHub `Prepare Debian Package` + `Create Debian Package`
# steps from .github/workflows/build-deb.yml, but locally. Stages the contents
# of dist.tar.gz under pkgroot/home/edge/agent, writes a DEBIAN/control file
# with the requested version, and runs dpkg-deb --build. Matches the same
# layout the jiro4989/build-deb-action@v3 action produces in CI.
#
# $1 - debian package version string (e.g. "2.6.0.2.2-dnx").
deb_build_cmd() {
local version="$1"
cat <<EOF
set -euo pipefail
if [[ ! -f /work/dist.tar.gz ]]; then
echo "error: /work/dist.tar.gz missing — run 'docker/build.sh' first" >&2
exit 1
fi
cd /work
rm -rf pkgroot/home pkgroot/DEBIAN/control
mkdir -p pkgroot/home/edge pkgroot/DEBIAN
tar -xzf dist.tar.gz -C pkgroot/home/edge/
mv pkgroot/home/edge/dist pkgroot/home/edge/agent
mv pkgroot/home/edge/agent/share/mtconnect/* pkgroot/home/edge/agent/
rm -rf pkgroot/home/edge/agent/share \
pkgroot/home/edge/agent/docker \
pkgroot/home/edge/agent/demo
# Generated dynamically — pkgroot/DEBIAN/control is intentionally not in git
# (see commit da3e934e) since CI builds it via jiro4989/build-deb-action.
cat > pkgroot/DEBIAN/control <<CTRL
Package: mtconnect-agent
Version: ${version}
Section: misc
Priority: optional
Architecture: all
Maintainer: Datanomix <support@datanomix.io>
Description: MTConnect Agent for Linux
CTRL
out="mtconnect-agent_${version}_all.deb"
dpkg-deb --build pkgroot "\$out"
echo ">>> built /work/\$out"
ls -lh "\$out"
EOF
}

# Pick the command to run inside the container.
if [[ $# -eq 0 ]]; then
container_cmd=(bash -lc "${CI_BUILD_CMD}")
elif [[ "$1" == "shell" ]]; then
container_cmd=(bash)
elif [[ "$1" == "deb" ]]; then
deb_version="${2:-0.0.0.0}"
container_cmd=(bash -lc "$(deb_build_cmd "${deb_version}")")
elif [[ "$1" == "--" ]]; then
shift
container_cmd=("$@")
else
container_cmd=("$@")
fi

# Only attach a TTY if we have one (so the script also works under CI / nohup).
tty_flags=(--rm)
if [[ -t 0 && -t 1 ]]; then
tty_flags+=(-it)
fi

# Run as the host user so build outputs aren't owned by root, and override
# HOME to the conan cache volume so conan can write its package cache there.
exec docker run "${tty_flags[@]}" \
--user "$(id -u):$(id -g)" \
-e HOME=/conan-home \
-v "${REPO_ROOT}:/work" \
-v "${CACHE_DIR}:/conan-home" \
-w /work \
"${IMAGE_TAG}" \
"${container_cmd[@]}"
28 changes: 15 additions & 13 deletions src/mtconnect/entity/xml_printer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ namespace mtconnect {
{
if (!attr.second.empty())
{
THROW_IF_XML2_ERROR(xmlTextWriterWriteAttribute(writer, BAD_CAST attr.first.c_str(),
BAD_CAST attr.second.c_str()));
THROW_IF_XML2_ERROR(
writeXmlAttribute(writer, attr.first.c_str(), attr.second.c_str()));
}
}
}
Expand All @@ -64,14 +64,16 @@ namespace mtconnect {

if (!body.empty())
{
xmlChar *text = nullptr;
if (!raw)
text = xmlEncodeEntitiesReentrant(nullptr, BAD_CAST body.c_str());
else
text = BAD_CAST body.c_str();
THROW_IF_XML2_ERROR(xmlTextWriterWriteRaw(writer, text));
if (!raw)
{
xmlChar *text = xmlEncodeEntitiesReentrant(nullptr, BAD_CAST body.c_str());
THROW_IF_XML2_ERROR(writeXmlRaw(writer, reinterpret_cast<const char *>(text)));
xmlFree(text);
}
else
{
THROW_IF_XML2_ERROR(writeXmlRaw(writer, body.c_str()));
}
}
}

Expand Down Expand Up @@ -160,20 +162,20 @@ namespace mtconnect {
{
// The value is the content for a simple element
if (*s != '\0')
THROW_IF_XML2_ERROR(xmlTextWriterWriteString(writer, BAD_CAST s));
THROW_IF_XML2_ERROR(writeXmlString(writer, s));
}
else if (p.first == "RAW")
{
if (*s != '\0')
THROW_IF_XML2_ERROR(xmlTextWriterWriteRaw(writer, BAD_CAST s));
THROW_IF_XML2_ERROR(writeXmlRaw(writer, s));
}
else
{
QName name(p.first);
string qname = stripUndeclaredNamespace(name, namespaces);
AutoElement element(writer, qname);
if (*s != '\0')
THROW_IF_XML2_ERROR(xmlTextWriterWriteString(writer, BAD_CAST s));
THROW_IF_XML2_ERROR(writeXmlString(writer, s));
}
}

Expand Down Expand Up @@ -246,8 +248,8 @@ namespace mtconnect {
bool isNsDecl = name.hasNs() && name.getNs() == "xmlns";
if (!isNsDecl || namespaces.count(string(name.getName())) == 0)
{
THROW_IF_XML2_ERROR(xmlTextWriterWriteAttribute(writer, BAD_CAST a.first.c_str(),
BAD_CAST toCharPtr(a.second, t)));
THROW_IF_XML2_ERROR(
writeXmlAttribute(writer, a.first.c_str(), toCharPtr(a.second, t)));
}
}

Expand Down
Loading
Loading