功能: 服务端生成完整报告并保护单任务
- 使用 libxlsxwriter 常量内存导出 Excel\n- 增加全局任务占用提示与原始数据持久化\n- 保留运行时任务数据并更新接口文档
@@ -36,6 +36,13 @@ endif()
|
||||
find_package(Drogon CONFIG REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# libxlsxwriter 使用常量内存模式把大型工作簿逐行落盘,避免报告导出占满服务进程内存。
|
||||
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(${REPO_ROOT}/third_party/libxlsxwriter_repo
|
||||
${CMAKE_CURRENT_BINARY_DIR}/libxlsxwriter)
|
||||
|
||||
file(GLOB_RECURSE SOURCES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
|
||||
@@ -54,6 +61,7 @@ target_include_directories(wind_server PRIVATE
|
||||
target_link_libraries(wind_server PRIVATE
|
||||
Drogon::Drogon
|
||||
Threads::Threads
|
||||
xlsxwriter
|
||||
)
|
||||
|
||||
# 运行期 .so 解析:RPATH 指向 Drogon install 的 libs 目录
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <set>
|
||||
@@ -17,6 +18,11 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <xlsxwriter/format.h>
|
||||
#include <xlsxwriter/workbook.h>
|
||||
#include <xlsxwriter/worksheet.h>
|
||||
#include <drogon/utils/Utilities.h>
|
||||
|
||||
using json = nlohmann::json;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -25,6 +31,9 @@ namespace {
|
||||
constexpr int kErrorInvalidRequest = 1001;
|
||||
constexpr int kErrorJobNotFound = 1002;
|
||||
constexpr int kErrorServer = 1003;
|
||||
constexpr int kErrorJobBusy = 1004;
|
||||
constexpr auto kUploadIdleTimeout = std::chrono::minutes(30);
|
||||
constexpr auto kCompletedJobRetention = std::chrono::hours(24);
|
||||
constexpr const char* kDefaultSchemeId = "scheme_one";
|
||||
constexpr const char* kSchemeTwoId = "scheme_two";
|
||||
constexpr const char* kSchemeOneName = "方案一";
|
||||
@@ -65,15 +74,17 @@ struct RemovedPoint {
|
||||
struct CalculationOptions {
|
||||
std::string scheme_id = kDefaultSchemeId;
|
||||
double rated_power = 4800.0;
|
||||
double rated_wind_speed = 18.0;
|
||||
double rated_wind_speed = 14.0;
|
||||
double power_step = 5.0;
|
||||
double cleaning_wind_speed_step = 0.25;
|
||||
double curve_wind_speed_step = 0.5;
|
||||
double wind_speed_change_threshold = 1.0;
|
||||
double iqr_lower_multiplier = 1.8;
|
||||
double iqr_lower_multiplier = 1.2;
|
||||
double iqr_upper_multiplier = 2.0;
|
||||
double minimum_generator_speed = 1.0;
|
||||
double generator_speed_k = 0.9;
|
||||
double rotor_radius = 78.0;
|
||||
double gearbox_ratio = 162.0;
|
||||
double grid_connected_speed = 0.0;
|
||||
double rated_generator_speed = 0.0;
|
||||
bool rated_power_provided = false;
|
||||
@@ -97,6 +108,17 @@ struct SchemeInfo {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
double scheme_one_rated_power = 4800.0;
|
||||
double scheme_one_rated_wind_speed = 14.0;
|
||||
double scheme_one_power_step = 5.0;
|
||||
double scheme_one_cleaning_wind_speed_step = 0.25;
|
||||
double scheme_one_wind_speed_change_threshold = 1.0;
|
||||
double scheme_one_iqr_lower_multiplier = 1.2;
|
||||
double scheme_one_iqr_upper_multiplier = 2.0;
|
||||
double scheme_one_minimum_generator_speed = 1.0;
|
||||
double scheme_one_generator_speed_k = 0.9;
|
||||
double scheme_one_rotor_radius = 78.0;
|
||||
double scheme_one_gearbox_ratio = 162.0;
|
||||
double grid_connected_speed = 1030.0;
|
||||
double rated_generator_speed = 1755.0;
|
||||
double rated_power = 2000.0;
|
||||
@@ -137,6 +159,123 @@ fs::path JobRowsPath(const std::string& job_id) {
|
||||
return JobDir(job_id) / "rows.jsonl";
|
||||
}
|
||||
|
||||
fs::path JobRawRowsPath(const std::string& job_id) {
|
||||
return JobDir(job_id) / "raw_rows.jsonl";
|
||||
}
|
||||
|
||||
fs::path JobResultPath(const std::string& job_id) {
|
||||
return JobDir(job_id) / "result.json";
|
||||
}
|
||||
|
||||
std::mutex g_task_mutex;
|
||||
std::string g_active_job_id;
|
||||
std::chrono::steady_clock::time_point g_active_since;
|
||||
bool g_active_is_upload = false;
|
||||
|
||||
bool IsTaskBusyFor(const std::string& job_id) {
|
||||
std::lock_guard<std::mutex> lock(g_task_mutex);
|
||||
return !g_active_job_id.empty() && g_active_job_id != job_id;
|
||||
}
|
||||
|
||||
bool AcquireTask(const std::string& job_id, bool upload) {
|
||||
std::lock_guard<std::mutex> lock(g_task_mutex);
|
||||
if (!g_active_job_id.empty() && g_active_job_id != job_id) return false;
|
||||
g_active_job_id = job_id;
|
||||
g_active_is_upload = upload;
|
||||
g_active_since = std::chrono::steady_clock::now();
|
||||
return true;
|
||||
}
|
||||
|
||||
void TouchTask(const std::string& job_id) {
|
||||
std::lock_guard<std::mutex> lock(g_task_mutex);
|
||||
if (g_active_job_id == job_id) g_active_since = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
void ReleaseTask(const std::string& job_id) {
|
||||
std::lock_guard<std::mutex> lock(g_task_mutex);
|
||||
if (g_active_job_id == job_id) {
|
||||
g_active_job_id.clear();
|
||||
g_active_is_upload = false;
|
||||
}
|
||||
}
|
||||
|
||||
class TaskReleaseGuard {
|
||||
public:
|
||||
explicit TaskReleaseGuard(std::string job_id) : job_id_(std::move(job_id)) {}
|
||||
~TaskReleaseGuard() { ReleaseTask(job_id_); }
|
||||
private:
|
||||
std::string job_id_;
|
||||
};
|
||||
|
||||
void ExpireIdleUploadTask() {
|
||||
std::string expired;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_task_mutex);
|
||||
if (g_active_is_upload && !g_active_job_id.empty() &&
|
||||
std::chrono::steady_clock::now() - g_active_since > kUploadIdleTimeout) {
|
||||
expired = g_active_job_id;
|
||||
g_active_job_id.clear();
|
||||
g_active_is_upload = false;
|
||||
}
|
||||
}
|
||||
if (!expired.empty()) {
|
||||
std::error_code ignored;
|
||||
fs::remove_all(JobDir(expired), ignored);
|
||||
}
|
||||
}
|
||||
|
||||
void CleanupExpiredCompletedJobs() {
|
||||
std::error_code error;
|
||||
if (!fs::exists(JobsRoot(), error)) return;
|
||||
const auto now = fs::file_time_type::clock::now();
|
||||
for (const auto& entry : fs::directory_iterator(JobsRoot(), error)) {
|
||||
if (error || !entry.is_directory()) continue;
|
||||
const auto job_id = entry.path().filename().string();
|
||||
if (IsTaskBusyFor(job_id) || !fs::exists(entry.path() / "result.json")) continue;
|
||||
const auto modified = fs::last_write_time(entry.path(), error);
|
||||
if (!error && now - modified > kCompletedJobRetention) fs::remove_all(entry.path(), error);
|
||||
error.clear();
|
||||
}
|
||||
}
|
||||
|
||||
std::string ExcelColumnName(size_t index) {
|
||||
std::string name;
|
||||
for (size_t value = index + 1; value > 0; value = (value - 1) / 26) {
|
||||
name.insert(name.begin(), static_cast<char>('A' + (value - 1) % 26));
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
void WriteJsonCell(lxw_worksheet* sheet, lxw_row_t row, lxw_col_t column,
|
||||
const json& value, lxw_format* format = nullptr) {
|
||||
if (value.is_number()) {
|
||||
worksheet_write_number(sheet, row, column, value.get<double>(), format);
|
||||
} else if (value.is_boolean()) {
|
||||
worksheet_write_boolean(sheet, row, column, value.get<bool>(), format);
|
||||
} else if (!value.is_null()) {
|
||||
const auto text = value.is_string() ? value.get<std::string>() : value.dump();
|
||||
worksheet_write_string(sheet, row, column, text.c_str(), format);
|
||||
}
|
||||
}
|
||||
|
||||
std::string JsonText(const json& value) {
|
||||
if (value.is_string()) return value.get<std::string>();
|
||||
if (value.is_number_integer()) return std::to_string(value.get<long long>());
|
||||
if (value.is_number_unsigned()) return std::to_string(value.get<unsigned long long>());
|
||||
if (value.is_number_float()) {
|
||||
std::ostringstream output;
|
||||
output << value.get<double>();
|
||||
return output.str();
|
||||
}
|
||||
return value.is_null() ? "" : value.dump();
|
||||
}
|
||||
|
||||
std::string FileNameForFan(const std::string& fan_id) {
|
||||
std::string output;
|
||||
for (const auto ch : fan_id) output += std::isalnum(static_cast<unsigned char>(ch)) ? ch : '_';
|
||||
return output.empty() ? "wind_turbine" : output;
|
||||
}
|
||||
|
||||
fs::path SchemeConfigPath() {
|
||||
return fs::path("data") / "wind_schemes.json";
|
||||
}
|
||||
@@ -347,10 +486,16 @@ std::string NormalizeSchemeId(const std::string& scheme_id) {
|
||||
}
|
||||
|
||||
std::vector<SchemeInfo> DefaultSchemes() {
|
||||
return {
|
||||
{kDefaultSchemeId, kSchemeOneName, kSchemeOneDefaultDescription},
|
||||
{kSchemeTwoId, kSchemeTwoName, kSchemeTwoDefaultDescription, 1030.0, 1755.0, 2000.0},
|
||||
};
|
||||
SchemeInfo scheme_one;
|
||||
scheme_one.id = kDefaultSchemeId;
|
||||
scheme_one.name = kSchemeOneName;
|
||||
scheme_one.description = kSchemeOneDefaultDescription;
|
||||
|
||||
SchemeInfo scheme_two;
|
||||
scheme_two.id = kSchemeTwoId;
|
||||
scheme_two.name = kSchemeTwoName;
|
||||
scheme_two.description = kSchemeTwoDefaultDescription;
|
||||
return {scheme_one, scheme_two};
|
||||
}
|
||||
|
||||
std::optional<SchemeInfo> FindScheme(const std::vector<SchemeInfo>& schemes,
|
||||
@@ -369,7 +514,21 @@ json SchemeToJson(const SchemeInfo& scheme) {
|
||||
data["id"] = scheme.id;
|
||||
data["name"] = scheme.name;
|
||||
data["description"] = scheme.description;
|
||||
if (scheme.id == kSchemeTwoId) {
|
||||
if (scheme.id == kDefaultSchemeId) {
|
||||
data["parameters"] = {
|
||||
{"rated_power", scheme.scheme_one_rated_power},
|
||||
{"rated_wind_speed", scheme.scheme_one_rated_wind_speed},
|
||||
{"power_step", scheme.scheme_one_power_step},
|
||||
{"cleaning_wind_speed_step", scheme.scheme_one_cleaning_wind_speed_step},
|
||||
{"wind_speed_change_threshold", scheme.scheme_one_wind_speed_change_threshold},
|
||||
{"iqr_lower_multiplier", scheme.scheme_one_iqr_lower_multiplier},
|
||||
{"iqr_upper_multiplier", scheme.scheme_one_iqr_upper_multiplier},
|
||||
{"minimum_generator_speed", scheme.scheme_one_minimum_generator_speed},
|
||||
{"generator_speed_k", scheme.scheme_one_generator_speed_k},
|
||||
{"rotor_radius", scheme.scheme_one_rotor_radius},
|
||||
{"gearbox_ratio", scheme.scheme_one_gearbox_ratio},
|
||||
};
|
||||
} else if (scheme.id == kSchemeTwoId) {
|
||||
data["parameters"]["grid_connected_speed"] = scheme.grid_connected_speed;
|
||||
data["parameters"]["rated_generator_speed"] = scheme.rated_generator_speed;
|
||||
data["parameters"]["rated_power"] = scheme.rated_power;
|
||||
@@ -401,10 +560,37 @@ std::vector<SchemeInfo> LoadSchemes() {
|
||||
if (description.has_value()) {
|
||||
scheme.description = description.value();
|
||||
}
|
||||
if (scheme.id == kSchemeTwoId &&
|
||||
item.contains("parameters") &&
|
||||
item["parameters"].is_object()) {
|
||||
const auto& params = item["parameters"];
|
||||
if (!item.contains("parameters") || !item["parameters"].is_object()) {
|
||||
continue;
|
||||
}
|
||||
const auto& params = item["parameters"];
|
||||
if (scheme.id == kDefaultSchemeId) {
|
||||
const auto load_positive = [&](const std::string& field, double& target) {
|
||||
if (const auto value = GetNumberField(params, field);
|
||||
value.has_value() && std::isfinite(value.value()) && value.value() > 0.0) {
|
||||
target = value.value();
|
||||
}
|
||||
};
|
||||
const auto load_non_negative = [&](const std::string& field, double& target) {
|
||||
if (const auto value = GetNumberField(params, field);
|
||||
value.has_value() && std::isfinite(value.value()) && value.value() >= 0.0) {
|
||||
target = value.value();
|
||||
}
|
||||
};
|
||||
load_positive("rated_power", scheme.scheme_one_rated_power);
|
||||
load_positive("rated_wind_speed", scheme.scheme_one_rated_wind_speed);
|
||||
load_positive("power_step", scheme.scheme_one_power_step);
|
||||
load_positive("cleaning_wind_speed_step", scheme.scheme_one_cleaning_wind_speed_step);
|
||||
load_non_negative("wind_speed_change_threshold",
|
||||
scheme.scheme_one_wind_speed_change_threshold);
|
||||
load_non_negative("iqr_lower_multiplier", scheme.scheme_one_iqr_lower_multiplier);
|
||||
load_non_negative("iqr_upper_multiplier", scheme.scheme_one_iqr_upper_multiplier);
|
||||
load_non_negative("minimum_generator_speed",
|
||||
scheme.scheme_one_minimum_generator_speed);
|
||||
load_non_negative("generator_speed_k", scheme.scheme_one_generator_speed_k);
|
||||
load_positive("rotor_radius", scheme.scheme_one_rotor_radius);
|
||||
load_positive("gearbox_ratio", scheme.scheme_one_gearbox_ratio);
|
||||
} else if (scheme.id == kSchemeTwoId) {
|
||||
if (const auto value = GetNumberField(params, "grid_connected_speed");
|
||||
value.has_value() && value.value() > 0.0) {
|
||||
scheme.grid_connected_speed = value.value();
|
||||
@@ -624,6 +810,14 @@ CalculationOptions ParseOptions(const json& body) {
|
||||
value.has_value() && value.value() >= 0.0) {
|
||||
options.generator_speed_k = value.value();
|
||||
}
|
||||
if (const auto value = GetDoubleField(opt, "rotor_radius");
|
||||
value.has_value() && value.value() > 0.0) {
|
||||
options.rotor_radius = value.value();
|
||||
}
|
||||
if (const auto value = GetDoubleField(opt, "gearbox_ratio");
|
||||
value.has_value() && value.value() > 0.0) {
|
||||
options.gearbox_ratio = value.value();
|
||||
}
|
||||
if (const auto value = GetDoubleField(opt, "grid_connected_speed");
|
||||
value.has_value() && value.value() > 0.0) {
|
||||
options.grid_connected_speed = value.value();
|
||||
@@ -1161,10 +1355,43 @@ void WindPowerController::SaveSchemeDescription(
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<json> scheme_one_parameters;
|
||||
std::optional<double> grid_connected_speed;
|
||||
std::optional<double> rated_generator_speed;
|
||||
std::optional<double> rated_power;
|
||||
if (normalized_id == kSchemeTwoId && body.value().contains("parameters")) {
|
||||
if (normalized_id == kDefaultSchemeId) {
|
||||
if (!body.value().contains("parameters") || !body.value()["parameters"].is_object()) {
|
||||
SendError(callback, kErrorInvalidRequest, "方案一参数格式错误");
|
||||
return;
|
||||
}
|
||||
const auto& params = body.value()["parameters"];
|
||||
const auto valid_positive = [&](const std::string& field) {
|
||||
const auto value = GetDoubleField(params, field);
|
||||
return value.has_value() && std::isfinite(value.value()) && value.value() > 0.0;
|
||||
};
|
||||
const auto valid_non_negative = [&](const std::string& field) {
|
||||
const auto value = GetDoubleField(params, field);
|
||||
return value.has_value() && std::isfinite(value.value()) && value.value() >= 0.0;
|
||||
};
|
||||
if (!valid_positive("rated_power") || !valid_positive("rated_wind_speed") ||
|
||||
!valid_positive("power_step") || !valid_positive("cleaning_wind_speed_step") ||
|
||||
!valid_positive("rotor_radius") || !valid_positive("gearbox_ratio") ||
|
||||
!valid_non_negative("wind_speed_change_threshold") ||
|
||||
!valid_non_negative("iqr_lower_multiplier") ||
|
||||
!valid_non_negative("iqr_upper_multiplier") ||
|
||||
!valid_non_negative("minimum_generator_speed") ||
|
||||
!valid_non_negative("generator_speed_k")) {
|
||||
SendError(callback, kErrorInvalidRequest, "方案一参数必须为合法数值");
|
||||
return;
|
||||
}
|
||||
if (GetDoubleField(params, "cleaning_wind_speed_step").value() > 2.0 ||
|
||||
GetDoubleField(params, "iqr_lower_multiplier").value() > 10.0 ||
|
||||
GetDoubleField(params, "iqr_upper_multiplier").value() > 10.0) {
|
||||
SendError(callback, kErrorInvalidRequest, "方案一参数超出允许范围");
|
||||
return;
|
||||
}
|
||||
scheme_one_parameters = params;
|
||||
} else if (normalized_id == kSchemeTwoId && body.value().contains("parameters")) {
|
||||
if (!body.value()["parameters"].is_object()) {
|
||||
SendError(callback, kErrorInvalidRequest, "方案参数格式错误");
|
||||
return;
|
||||
@@ -1186,7 +1413,26 @@ void WindPowerController::SaveSchemeDescription(
|
||||
for (auto& scheme : schemes) {
|
||||
if (scheme.id == normalized_id) {
|
||||
scheme.description = description.value();
|
||||
if (scheme.id == kSchemeTwoId) {
|
||||
if (scheme.id == kDefaultSchemeId) {
|
||||
const auto& params = scheme_one_parameters.value();
|
||||
scheme.scheme_one_rated_power = GetDoubleField(params, "rated_power").value();
|
||||
scheme.scheme_one_rated_wind_speed = GetDoubleField(params, "rated_wind_speed").value();
|
||||
scheme.scheme_one_power_step = GetDoubleField(params, "power_step").value();
|
||||
scheme.scheme_one_cleaning_wind_speed_step =
|
||||
GetDoubleField(params, "cleaning_wind_speed_step").value();
|
||||
scheme.scheme_one_wind_speed_change_threshold =
|
||||
GetDoubleField(params, "wind_speed_change_threshold").value();
|
||||
scheme.scheme_one_iqr_lower_multiplier =
|
||||
GetDoubleField(params, "iqr_lower_multiplier").value();
|
||||
scheme.scheme_one_iqr_upper_multiplier =
|
||||
GetDoubleField(params, "iqr_upper_multiplier").value();
|
||||
scheme.scheme_one_minimum_generator_speed =
|
||||
GetDoubleField(params, "minimum_generator_speed").value();
|
||||
scheme.scheme_one_generator_speed_k =
|
||||
GetDoubleField(params, "generator_speed_k").value();
|
||||
scheme.scheme_one_rotor_radius = GetDoubleField(params, "rotor_radius").value();
|
||||
scheme.scheme_one_gearbox_ratio = GetDoubleField(params, "gearbox_ratio").value();
|
||||
} else if (scheme.id == kSchemeTwoId) {
|
||||
if (grid_connected_speed.has_value()) {
|
||||
scheme.grid_connected_speed = grid_connected_speed.value();
|
||||
}
|
||||
@@ -1268,9 +1514,16 @@ void WindPowerController::StartJob(
|
||||
return;
|
||||
}
|
||||
|
||||
ExpireIdleUploadTask();
|
||||
CleanupExpiredCompletedJobs();
|
||||
const auto job_id = GenerateJobId();
|
||||
if (!AcquireTask(job_id, true)) {
|
||||
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
fs::create_directories(JobsRoot());
|
||||
const auto job_id = GenerateJobId();
|
||||
fs::create_directories(JobDir(job_id));
|
||||
|
||||
std::ofstream meta(JobDir(job_id) / "metadata.json", std::ios::trunc);
|
||||
@@ -1279,11 +1532,14 @@ void WindPowerController::StartJob(
|
||||
|
||||
std::ofstream rows(JobRowsPath(job_id), std::ios::trunc);
|
||||
rows.close();
|
||||
std::ofstream raw_rows(JobRawRowsPath(job_id), std::ios::trunc);
|
||||
raw_rows.close();
|
||||
|
||||
json data;
|
||||
data["job_id"] = job_id;
|
||||
SendSuccess(callback, data);
|
||||
} catch (const std::exception&) {
|
||||
ReleaseTask(job_id);
|
||||
SendError(callback, kErrorServer, "创建计算任务失败");
|
||||
}
|
||||
}
|
||||
@@ -1308,9 +1564,13 @@ void WindPowerController::UploadChunk(
|
||||
SendError(callback, kErrorInvalidRequest, "缺少 rows 参数");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsTaskBusyFor(job_id.value())) {
|
||||
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
std::ofstream out(JobRowsPath(job_id.value()), std::ios::app);
|
||||
std::ofstream raw_out(JobRawRowsPath(job_id.value()), std::ios::app);
|
||||
int accepted = 0;
|
||||
for (const auto& row : (*body)["rows"]) {
|
||||
if (!row.is_object()) {
|
||||
@@ -1320,6 +1580,13 @@ void WindPowerController::UploadChunk(
|
||||
++accepted;
|
||||
}
|
||||
out.close();
|
||||
if (body->contains("raw_rows") && (*body)["raw_rows"].is_array()) {
|
||||
for (const auto& row : (*body)["raw_rows"]) {
|
||||
if (row.is_object()) raw_out << row.dump() << '\n';
|
||||
}
|
||||
}
|
||||
raw_out.close();
|
||||
TouchTask(job_id.value());
|
||||
|
||||
json data;
|
||||
data["accepted_rows"] = accepted;
|
||||
@@ -1345,6 +1612,11 @@ void WindPowerController::FinishJob(
|
||||
SendError(callback, kErrorJobNotFound, "计算任务不存在");
|
||||
return;
|
||||
}
|
||||
if (IsTaskBusyFor(job_id.value())) {
|
||||
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
|
||||
return;
|
||||
}
|
||||
TaskReleaseGuard finish_guard(job_id.value());
|
||||
|
||||
const CalculationOptions options = ParseOptions(*body);
|
||||
if (IsSchemeTwo(options) &&
|
||||
@@ -1526,7 +1798,8 @@ void WindPowerController::FinishJob(
|
||||
limit_power_count += removed;
|
||||
|
||||
for (auto& row : fan_rows) {
|
||||
row.tip_speed_ratio = row.generator_speed * 3.14 * 162.0 * 78.0 * 30.0 /
|
||||
row.tip_speed_ratio = row.generator_speed * 3.14 * options.gearbox_ratio *
|
||||
options.rotor_radius * 30.0 /
|
||||
row.wind_speed;
|
||||
}
|
||||
|
||||
@@ -1700,14 +1973,182 @@ void WindPowerController::FinishJob(
|
||||
data["estimated_params"] = estimated_params;
|
||||
|
||||
try {
|
||||
fs::remove_all(JobDir(job_id.value()));
|
||||
std::ofstream result_file(JobResultPath(job_id.value()), std::ios::trunc);
|
||||
result_file << data.dump();
|
||||
} catch (const std::exception&) {
|
||||
// 任务结果已经生成,临时文件清理失败不影响本次响应。
|
||||
SendError(callback, kErrorServer, "保存计算结果失败");
|
||||
return;
|
||||
}
|
||||
|
||||
SendSuccess(callback, data);
|
||||
}
|
||||
|
||||
void WindPowerController::ExportReport(
|
||||
const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& job_id) {
|
||||
if (!IsSafeJobId(job_id) || !fs::exists(JobResultPath(job_id))) {
|
||||
SendError(callback, kErrorJobNotFound, "计算任务不存在或结果已过期");
|
||||
return;
|
||||
}
|
||||
std::string error;
|
||||
const auto body = ParseBody(req, error);
|
||||
if (!body.has_value()) {
|
||||
SendError(callback, kErrorInvalidRequest, error);
|
||||
return;
|
||||
}
|
||||
const auto fan_id = GetStringField(*body, "fan_id");
|
||||
if (!fan_id.has_value() || !body->contains("effective_rows") ||
|
||||
!(*body)["effective_rows"].is_array() || !body->contains("report_rows") ||
|
||||
!(*body)["report_rows"].is_array()) {
|
||||
SendError(callback, kErrorInvalidRequest, "报告参数不完整");
|
||||
return;
|
||||
}
|
||||
if (!AcquireTask(job_id, false)) {
|
||||
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
|
||||
return;
|
||||
}
|
||||
TaskReleaseGuard report_guard(job_id);
|
||||
|
||||
try {
|
||||
json metadata;
|
||||
json result;
|
||||
{ std::ifstream input(JobDir(job_id) / "metadata.json"); input >> metadata; }
|
||||
{ std::ifstream input(JobResultPath(job_id)); input >> result; }
|
||||
const auto headers = metadata.value("raw_headers", json::array());
|
||||
const auto mapping = metadata.value("mapping", json::object());
|
||||
const auto fan_header = mapping.value("fan_id", "");
|
||||
const auto wind_header = mapping.value("wind_speed", "");
|
||||
if (!headers.is_array() || fan_header.empty() || wind_header.empty()) {
|
||||
SendError(callback, kErrorServer, "任务未保存原始数据列,无法导出完整报告");
|
||||
return;
|
||||
}
|
||||
size_t fan_column = headers.size();
|
||||
size_t wind_column = headers.size();
|
||||
for (size_t i = 0; i < headers.size(); ++i) {
|
||||
const auto header = headers[i].is_string() ? headers[i].get<std::string>() : "";
|
||||
if (header == fan_header) fan_column = i;
|
||||
if (header == wind_header) wind_column = i;
|
||||
}
|
||||
if (fan_column == headers.size() || wind_column == headers.size()) {
|
||||
SendError(callback, kErrorServer, "原始数据缺少风机编号或风速列");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto report_path = JobDir(job_id) / ("report_" + FileNameForFan(fan_id.value()) + ".xlsx");
|
||||
lxw_workbook_options options{};
|
||||
const auto temp_dir = JobDir(job_id).string();
|
||||
options.constant_memory = LXW_TRUE;
|
||||
options.use_zip64 = LXW_TRUE;
|
||||
options.tmpdir = const_cast<char*>(temp_dir.c_str());
|
||||
lxw_workbook* workbook = workbook_new_opt(report_path.string().c_str(), &options);
|
||||
if (!workbook) throw std::runtime_error("无法创建 Excel 工作簿");
|
||||
lxw_format* header_format = workbook_add_format(workbook);
|
||||
format_set_bold(header_format);
|
||||
format_set_bg_color(header_format, 0xE2E8F0);
|
||||
format_set_align(header_format, LXW_ALIGN_CENTER);
|
||||
lxw_format* number_format = workbook_add_format(workbook);
|
||||
format_set_num_format(number_format, "0.0000");
|
||||
|
||||
lxw_worksheet* detail = workbook_add_worksheet(workbook, "筛选后的数据");
|
||||
worksheet_freeze_panes(detail, 1, 0);
|
||||
const bool include_pitch = result.value("scheme", json::object()).value("id", "") == kSchemeTwoId;
|
||||
const std::vector<std::string> detail_headers = include_pitch
|
||||
? std::vector<std::string>{"风机编号", "采样时间", "平均功率", "平均转速", "平均风速", "3个叶片变桨角平均值"}
|
||||
: std::vector<std::string>{"风机编号", "采样时间", "平均功率", "平均转速", "平均风速"};
|
||||
for (size_t i = 0; i < detail_headers.size(); ++i) {
|
||||
worksheet_write_string(detail, 0, i, detail_headers[i].c_str(), header_format);
|
||||
worksheet_set_column(detail, i, i, i == 1 ? 22 : 16, nullptr);
|
||||
}
|
||||
lxw_row_t detail_row = 1;
|
||||
for (const auto& point : (*body)["effective_rows"]) {
|
||||
if (!point.is_object()) continue;
|
||||
worksheet_write_string(detail, detail_row, 0, point.value("fan_id", fan_id.value()).c_str(), nullptr);
|
||||
worksheet_write_string(detail, detail_row, 1, point.value("time", "").c_str(), nullptr);
|
||||
worksheet_write_number(detail, detail_row, 2, point.value("active_power", 0.0), number_format);
|
||||
worksheet_write_number(detail, detail_row, 3, point.value("generator_speed", 0.0), number_format);
|
||||
worksheet_write_number(detail, detail_row, 4, point.value("wind_speed", 0.0), number_format);
|
||||
if (include_pitch) worksheet_write_number(detail, detail_row, 5, point.value("pitch_angle_average", 0.0), number_format);
|
||||
++detail_row;
|
||||
}
|
||||
worksheet_autofilter(detail, 0, 0, std::max<lxw_row_t>(1, detail_row - 1), detail_headers.size() - 1);
|
||||
|
||||
lxw_worksheet* raw = workbook_add_worksheet(workbook, "筛选前的数据");
|
||||
worksheet_freeze_panes(raw, 1, 0);
|
||||
for (size_t i = 0; i < headers.size(); ++i) {
|
||||
const auto header = headers[i].is_string() ? headers[i].get<std::string>() : "";
|
||||
worksheet_write_string(raw, 0, i, header.c_str(), header_format);
|
||||
worksheet_set_column(raw, i, i, 16, nullptr);
|
||||
}
|
||||
std::set<std::string> source_files;
|
||||
lxw_row_t raw_row = 1;
|
||||
std::ifstream raw_input(JobRawRowsPath(job_id));
|
||||
std::string line;
|
||||
while (std::getline(raw_input, line)) {
|
||||
const auto source = json::parse(line, nullptr, false);
|
||||
if (source.is_discarded() || !source.contains("values") || !source["values"].is_array()) continue;
|
||||
const auto& values = source["values"];
|
||||
if (fan_column >= values.size() || JsonText(values[fan_column]) != fan_id.value()) continue;
|
||||
source_files.insert(source.value("file_name", ""));
|
||||
for (size_t column = 0; column < values.size(); ++column) WriteJsonCell(raw, raw_row, column, values[column]);
|
||||
++raw_row;
|
||||
}
|
||||
if (source_files.size() != 1) {
|
||||
workbook_close(workbook);
|
||||
SendError(callback, kErrorInvalidRequest, "完整报告要求当前风机对应唯一一份上传 Excel 文件");
|
||||
return;
|
||||
}
|
||||
worksheet_autofilter(raw, 0, 0, std::max<lxw_row_t>(1, raw_row - 1), headers.size() - 1);
|
||||
|
||||
lxw_worksheet* curve = workbook_add_worksheet(workbook, "功率曲线计算表");
|
||||
worksheet_freeze_panes(curve, 1, 0);
|
||||
const std::vector<std::string> curve_headers = {"序号", "风速 (m/s)", "风频时间 (h)", "计算功率 (kW)", "保证功率 (kW)", "计算发电量 (kWh)", "理论发电量 (kWh)", "", "K值"};
|
||||
for (size_t i = 0; i < curve_headers.size(); ++i) {
|
||||
worksheet_write_string(curve, 0, i, curve_headers[i].c_str(), header_format);
|
||||
worksheet_set_column(curve, i, i, i == 0 ? 9 : 17, nullptr);
|
||||
}
|
||||
const auto raw_wind = ExcelColumnName(wind_column);
|
||||
lxw_row_t curve_row = 1;
|
||||
for (const auto& point : (*body)["report_rows"]) {
|
||||
const auto excel_row = curve_row + 1;
|
||||
worksheet_write_number(curve, curve_row, 0, excel_row - 1, nullptr);
|
||||
worksheet_write_number(curve, curve_row, 1, point.value("wind_speed", 0.0), number_format);
|
||||
const auto frequency = "=(COUNTIFS('筛选前的数据'!" + raw_wind + ":" + raw_wind + ",\">=\"&B" + std::to_string(excel_row) + "-0.5,'筛选前的数据'!" + raw_wind + ":" + raw_wind + ",\"<\"&B" + std::to_string(excel_row) + "+0.5)/COUNT('筛选前的数据'!" + raw_wind + ":" + raw_wind + "))*8760";
|
||||
worksheet_write_formula(curve, curve_row, 2, frequency.c_str(), number_format);
|
||||
const auto actual = "=IFERROR(AVERAGEIFS('筛选后的数据'!$C:$C,'筛选后的数据'!$E:$E,\">=\"&B" + std::to_string(excel_row) + "-0.5,'筛选后的数据'!$E:$E,\"<\"&B" + std::to_string(excel_row) + "+0.5),0)";
|
||||
worksheet_write_formula(curve, curve_row, 3, actual.c_str(), number_format);
|
||||
worksheet_write_number(curve, curve_row, 4, point.value("design_power", 0.0), number_format);
|
||||
const auto generated = "=IFERROR(ROUND(C" + std::to_string(excel_row) + "*D" + std::to_string(excel_row) + "/1000,4),0)";
|
||||
const auto theoretical = "=ROUND(C" + std::to_string(excel_row) + "*E" + std::to_string(excel_row) + "/1000,4)";
|
||||
worksheet_write_formula(curve, curve_row, 5, generated.c_str(), number_format);
|
||||
worksheet_write_formula(curve, curve_row, 6, theoretical.c_str(), number_format);
|
||||
++curve_row;
|
||||
}
|
||||
const auto last_row = std::max<lxw_row_t>(2, curve_row);
|
||||
const auto k_formula = "=IFERROR(SUM(F2:F" + std::to_string(last_row) + ")/SUM(G2:G" + std::to_string(last_row) + "),0)";
|
||||
worksheet_write_formula(curve, 1, 8, k_formula.c_str(), number_format);
|
||||
worksheet_autofilter(curve, 0, 0, std::max<lxw_row_t>(1, curve_row - 1), 6);
|
||||
|
||||
if (body->contains("chart_image") && (*body)["chart_image"].is_string()) {
|
||||
auto image_data = (*body)["chart_image"].get<std::string>();
|
||||
const auto comma = image_data.find(',');
|
||||
if (comma != std::string::npos) image_data = image_data.substr(comma + 1);
|
||||
const auto image_path = JobDir(job_id) / "report_chart.png";
|
||||
std::ofstream image(image_path, std::ios::binary | std::ios::trunc);
|
||||
image << drogon::utils::base64Decode(image_data);
|
||||
image.close();
|
||||
worksheet_insert_image(curve, 3, 8, image_path.string().c_str());
|
||||
}
|
||||
if (workbook_close(workbook) != LXW_NO_ERROR) throw std::runtime_error("写入 Excel 文件失败");
|
||||
callback(HttpResponse::newFileResponse(report_path.string(),
|
||||
"完整功率曲线报告_" + FileNameForFan(fan_id.value()) + ".xlsx",
|
||||
CT_CUSTOM,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
|
||||
} catch (const std::exception&) {
|
||||
SendError(callback, kErrorServer, "完整报告生成失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
void WindPowerController::DeleteJob(
|
||||
const HttpRequestPtr&,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
@@ -1719,6 +2160,7 @@ void WindPowerController::DeleteJob(
|
||||
|
||||
try {
|
||||
fs::remove_all(JobDir(job_id));
|
||||
ReleaseTask(job_id);
|
||||
SendSuccess(callback);
|
||||
} catch (const std::exception&) {
|
||||
SendError(callback, kErrorServer, "清理任务失败");
|
||||
|
||||
@@ -14,6 +14,7 @@ public:
|
||||
ADD_METHOD_TO(WindPowerController::StartJob, "/api/wind/jobs/start", Post);
|
||||
ADD_METHOD_TO(WindPowerController::UploadChunk, "/api/wind/jobs/chunk", Post);
|
||||
ADD_METHOD_TO(WindPowerController::FinishJob, "/api/wind/jobs/finish", Post);
|
||||
ADD_METHOD_TO(WindPowerController::ExportReport, "/api/wind/jobs/{1}/report", Post);
|
||||
ADD_METHOD_TO(WindPowerController::DeleteJob, "/api/wind/jobs/{1}", Delete);
|
||||
ADD_METHOD_TO(WindPowerController::GetSchemes, "/api/wind/schemes", Get);
|
||||
ADD_METHOD_TO(WindPowerController::SaveSchemeDescription,
|
||||
@@ -38,6 +39,9 @@ public:
|
||||
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void FinishJob(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||
void ExportReport(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& job_id);
|
||||
void DeleteJob(const HttpRequestPtr& req,
|
||||
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||
const std::string& job_id);
|
||||
|
||||
@@ -93,7 +93,9 @@ fi
|
||||
|
||||
step "3/5 上传应用与 systemd 配置..."
|
||||
ssh_cmd 'command -v rsync >/dev/null || (sudo apt-get update -qq && sudo apt-get install -y -qq rsync >/dev/null)'
|
||||
sshpass -p "${SSH_PASSWORD}" rsync -az --delete -e "ssh ${SSH_OPTS[*]}" \
|
||||
# data/、uploads/ 和 logs/ 由远端服务在运行时维护;发布代码时必须保留它们。
|
||||
sshpass -p "${SSH_PASSWORD}" rsync -az --delete \
|
||||
--exclude '/data/' --exclude '/uploads/' --exclude '/logs/' -e "ssh ${SSH_OPTS[*]}" \
|
||||
"${LOCAL_APP_DIR}/" "${TARGET}:${REMOTE_APP_PATH}/"
|
||||
sshpass -p "${SSH_PASSWORD}" scp "${SSH_OPTS[@]}" \
|
||||
"${DEPLOY_STAGE}/${SERVICE_NAME}.service" \
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
### POST /api/wind/jobs/start
|
||||
|
||||
创建风功率计算任务。前端已完成 Excel 表头解析和字段映射,后端只保存任务元数据。
|
||||
创建风功率计算任务。服务器全局同一时刻仅允许一个上传、计算或报告生成任务;繁忙时返回 `status: 1004` 和“服务器正在处理数据,请等待当前任务完成”。
|
||||
|
||||
请求:
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
"wind_speed": "风速(m/s)",
|
||||
"active_power": "有功功率(kW)",
|
||||
"generator_speed": "发电机转速(rpm)"
|
||||
}
|
||||
},
|
||||
"raw_headers": ["时间", "风机编号", "风速(m/s)", "有功功率(kW)", "发电机转速(rpm)"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -89,7 +90,7 @@
|
||||
|
||||
### POST /api/wind/jobs/chunk
|
||||
|
||||
上传标准化后的数据分片。每行字段必须使用标准字段名。
|
||||
上传标准化后的数据分片。`raw_rows` 保存原始 Excel 行,用于服务端完整报告的“筛选前的数据”工作表。
|
||||
|
||||
请求:
|
||||
|
||||
@@ -105,6 +106,9 @@
|
||||
"active_power": 114.3471,
|
||||
"generator_speed": 1109.3143
|
||||
}
|
||||
],
|
||||
"raw_rows": [
|
||||
{"file_name": "01_风机历史数据.xls", "values": ["2024-09-01 14:00:00", "01#", 3.414, 114.3471, 1109.3143]}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -131,14 +135,18 @@
|
||||
{
|
||||
"job_id": "job_123",
|
||||
"options": {
|
||||
"rated_power": 4800,
|
||||
"rated_wind_speed": 14,
|
||||
"power_step": 5,
|
||||
"cleaning_wind_speed_step": 0.25,
|
||||
"curve_wind_speed_step": 0.5,
|
||||
"wind_speed_change_threshold": 1,
|
||||
"iqr_lower_multiplier": 1.8,
|
||||
"iqr_lower_multiplier": 1.2,
|
||||
"iqr_upper_multiplier": 2,
|
||||
"minimum_generator_speed": 1,
|
||||
"generator_speed_k": 0.9
|
||||
"generator_speed_k": 0.9,
|
||||
"rotor_radius": 78,
|
||||
"gearbox_ratio": 162
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -229,7 +237,7 @@
|
||||
- `wind_speed <= 0` 的数据剔除。
|
||||
- 同一风机同一时间重复记录保留第一条。
|
||||
- 限功率识别按参考脚本执行:按功率分箱、按日期分组,组内风速跨度大于阈值时剔除。
|
||||
- 叶尖速比使用参考脚本公式 `generator_speed * 3.14 * 162 * 78 * 30 / wind_speed`,按风速分箱做 IQR 清洗。
|
||||
- 叶尖速比按 `generator_speed * 3.14 * gearbox_ratio * rotor_radius * 30 / wind_speed` 计算;`3.14` 和 `30` 固定,叶轮半径与齿轮箱传动比由方案一参数提供,再按风速分箱做 IQR 清洗。
|
||||
- 风速-功率关系按同一风速分箱和 IQR 参数清洗。
|
||||
- 每台风机在 IQR 清洗后自动估算平台功率和平台起始风速,返回到 `estimated_params`。
|
||||
- 高风速平台区明显低于平台功率的点剔除为 `high_wind_low_power`。
|
||||
@@ -239,6 +247,12 @@
|
||||
- `scatter_points` 返回清洗后保留点,`filtered_points` 返回所有过滤阶段滤除的点和原因。
|
||||
- `estimated_params.source` 为 `auto`、`auto_power_fallback_wind` 或 `fallback`。
|
||||
|
||||
### POST /api/wind/jobs/{jobId}/report
|
||||
|
||||
服务端以常量内存方式生成完整报告并直接返回 `.xlsx` 附件。计算完成的数据保留 24 小时;上传任务连续 30 分钟未活动会自动清理。
|
||||
|
||||
请求体包含 `fan_id`、当前编辑后的 `effective_rows`、`report_rows` 和可选的 `chart_image`(PNG data URL)。成功时响应为 Excel 文件流;失败时仍使用统一 JSON 响应信封。
|
||||
|
||||
### DELETE /api/wind/jobs/{job_id}
|
||||
|
||||
清理未完成任务的临时文件。
|
||||
|
||||
@@ -254,6 +254,13 @@
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.schemeParamHint {
|
||||
margin: 14px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.schemeParamGrid input {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
@@ -805,6 +812,44 @@ td {
|
||||
.error { border-color: #fecaca; background: #fff1f2; color: #b91c1c; }
|
||||
.alert.info { border-color: #bfdbfe; background: #eff6ff; color: #1d4ed8; }
|
||||
|
||||
.dialogBackdrop {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
background: rgba(15, 23, 42, 0.42);
|
||||
}
|
||||
|
||||
.appDialog {
|
||||
width: min(440px, 100%);
|
||||
padding: 22px;
|
||||
border: 1px solid #dbe3ed;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
|
||||
.appDialog h2 {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.appDialog p {
|
||||
margin: 12px 0 0;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.appDialogActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.schemeDescriptionControl span,
|
||||
.fieldControl span { color: #334155; }
|
||||
.schemeDescriptionControl input,
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'uplot/dist/uPlot.min.css';
|
||||
import * as XLSX from 'xlsx';
|
||||
import {
|
||||
deleteWindJob,
|
||||
downloadWindReport,
|
||||
finishWindJob,
|
||||
getWindChartOptions,
|
||||
getWindSchemes,
|
||||
@@ -58,7 +59,24 @@ const EDIT_TOOL_ERASE = 'erase';
|
||||
const DEFAULT_SCHEME_ID = 'scheme_one';
|
||||
const SCHEME_TWO_ID = 'scheme_two';
|
||||
const DEFAULT_SCHEMES = [
|
||||
{ id: 'scheme_one', name: '方案一', description: '通用方案' },
|
||||
{
|
||||
id: 'scheme_one',
|
||||
name: '方案一',
|
||||
description: '通用方案',
|
||||
parameters: {
|
||||
rated_power: 4800,
|
||||
rated_wind_speed: 14,
|
||||
power_step: 5,
|
||||
cleaning_wind_speed_step: 0.25,
|
||||
wind_speed_change_threshold: 1,
|
||||
iqr_lower_multiplier: 1.2,
|
||||
iqr_upper_multiplier: 2,
|
||||
minimum_generator_speed: 1,
|
||||
generator_speed_k: 0.9,
|
||||
rotor_radius: 78,
|
||||
gearbox_ratio: 162,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: SCHEME_TWO_ID,
|
||||
name: '方案二',
|
||||
@@ -76,6 +94,32 @@ const DEFAULT_SCHEME_TWO_PARAMS = {
|
||||
rated_generator_speed: '1755',
|
||||
rated_power: '2000',
|
||||
};
|
||||
const DEFAULT_SCHEME_ONE_PARAMS = {
|
||||
rated_power: '4800',
|
||||
rated_wind_speed: '14',
|
||||
power_step: '5',
|
||||
cleaning_wind_speed_step: '0.25',
|
||||
wind_speed_change_threshold: '1',
|
||||
iqr_lower_multiplier: '1.2',
|
||||
iqr_upper_multiplier: '2',
|
||||
minimum_generator_speed: '1',
|
||||
generator_speed_k: '0.9',
|
||||
rotor_radius: '78',
|
||||
gearbox_ratio: '162',
|
||||
};
|
||||
const SCHEME_ONE_PARAM_FIELDS = [
|
||||
{ key: 'rated_power', label: '额定功率', unit: 'kW', min: '0', step: '0.1' },
|
||||
{ key: 'rated_wind_speed', label: '额定风速', unit: 'm/s', min: '0', step: '0.01' },
|
||||
{ key: 'power_step', label: '功率分箱步长', unit: 'kW', min: '0', step: '0.01' },
|
||||
{ key: 'cleaning_wind_speed_step', label: '清洗风速分箱步长', unit: 'm/s', min: '0', step: '0.01' },
|
||||
{ key: 'wind_speed_change_threshold', label: '风速变化阈值', unit: 'm/s', min: '0', step: '0.01' },
|
||||
{ key: 'iqr_lower_multiplier', label: 'IQR 下限系数', unit: '', min: '0', step: '0.01' },
|
||||
{ key: 'iqr_upper_multiplier', label: 'IQR 上限系数', unit: '', min: '0', step: '0.01' },
|
||||
{ key: 'minimum_generator_speed', label: '最小发电机转速', unit: 'rpm', min: '0', step: '0.01' },
|
||||
{ key: 'generator_speed_k', label: '发电机转速系数 K', unit: '', min: '0', step: '0.01' },
|
||||
{ key: 'rotor_radius', label: '叶轮半径', unit: 'm', min: '0', step: '0.01' },
|
||||
{ key: 'gearbox_ratio', label: '齿轮箱传动比', unit: '', min: '0', step: '0.01' },
|
||||
];
|
||||
const CHART_STATE_KEY = 'wind_power_chart_state_v1';
|
||||
const DEFAULT_CHART_OPTIONS = {
|
||||
title: '#机组功率曲线',
|
||||
@@ -393,11 +437,13 @@ function buildStandardRows(files, mapping) {
|
||||
);
|
||||
|
||||
const rows = [];
|
||||
const rawRows = [];
|
||||
for (const file of files) {
|
||||
const localIndexes = Object.fromEntries(
|
||||
REQUIRED_FIELDS.map((field) => [field.key, file.headers.indexOf(mapping[field.key])]),
|
||||
);
|
||||
for (const row of file.rows) {
|
||||
for (let rowIndex = 0; rowIndex < file.rows.length; rowIndex += 1) {
|
||||
const row = file.rows[rowIndex];
|
||||
rows.push({
|
||||
time: normalizeTime(getCell(row, localIndexes.time)),
|
||||
fan_id: String(getCell(row, localIndexes.fan_id) ?? '').trim(),
|
||||
@@ -408,10 +454,14 @@ function buildStandardRows(files, mapping) {
|
||||
blade_pitch_2: normalizeNumber(getCell(row, localIndexes.blade_pitch_2)),
|
||||
blade_pitch_3: normalizeNumber(getCell(row, localIndexes.blade_pitch_3)),
|
||||
});
|
||||
rawRows.push({
|
||||
file_name: file.file_name,
|
||||
values: [...(file.raw_rows?.[rowIndex] || row)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { rows, indexes };
|
||||
return { rows, rawRows, indexes };
|
||||
}
|
||||
|
||||
function hasMatchingHeaderSequence(files) {
|
||||
@@ -423,44 +473,6 @@ function hasMatchingHeaderSequence(files) {
|
||||
));
|
||||
}
|
||||
|
||||
function findRawSourceFiles(files, mapping, fanId) {
|
||||
if (!fanId) return [];
|
||||
return files.filter((file) => {
|
||||
const fanColumnIndex = file.headers.indexOf(mapping.fan_id);
|
||||
return fanColumnIndex >= 0 && file.rows.some((row) => (
|
||||
String(row[fanColumnIndex] ?? '').trim() === fanId
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
function buildRawReportData(file, mapping, fanId) {
|
||||
if (!file || !fanId) return null;
|
||||
|
||||
const fanColumnIndex = file.headers.indexOf(mapping.fan_id);
|
||||
const windSpeedColumnIndex = file.headers.indexOf(mapping.wind_speed);
|
||||
if (fanColumnIndex < 0 || windSpeedColumnIndex < 0) return null;
|
||||
|
||||
const rows = file.rows.reduce((result, row, index) => {
|
||||
if (String(row[fanColumnIndex] ?? '').trim() === fanId) {
|
||||
const rawRow = [...(file.raw_rows?.[index] || row)];
|
||||
const windSpeed = normalizeNumber(rawRow[windSpeedColumnIndex]);
|
||||
// 部分源 Excel 将风速保存为“数字文本”。保留显示值的同时写成数值单元格,
|
||||
// 使计算表的 COUNTIFS 能正确统计风频。
|
||||
if (Number.isFinite(windSpeed)) {
|
||||
rawRow[windSpeedColumnIndex] = windSpeed;
|
||||
}
|
||||
result.push(rawRow);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
headers: [...(file.raw_headers || file.headers)],
|
||||
rows,
|
||||
windSpeedColumnIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function formatTimestampForFile(date = new Date()) {
|
||||
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`
|
||||
+ `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
||||
@@ -585,6 +597,13 @@ function schemeParamsToState(parameters) {
|
||||
};
|
||||
}
|
||||
|
||||
function schemeOneParamsToState(parameters) {
|
||||
return Object.fromEntries(Object.entries(DEFAULT_SCHEME_ONE_PARAMS).map(([field, fallback]) => [
|
||||
field,
|
||||
String(parameters?.[field] ?? fallback),
|
||||
]));
|
||||
}
|
||||
|
||||
function SummaryCards({ summary }) {
|
||||
if (!summary) return null;
|
||||
const cards = [
|
||||
@@ -972,14 +991,25 @@ async function captureChartFrame(frame) {
|
||||
};
|
||||
}
|
||||
|
||||
function downloadReportBlob(blob, fanId) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `完整功率曲线报告_${sanitizeFileName(fanId)}_${formatTimestampForFile()}.xlsx`;
|
||||
link.click();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
||||
}
|
||||
|
||||
/* 已迁移到 reportExportWorker,由 Worker 承担 ExcelJS 的内存压力。 */
|
||||
/*
|
||||
const REPORT_DETAIL_HEADERS = [
|
||||
'风机编号',
|
||||
'采样时间',
|
||||
'平均功率',
|
||||
'平均转速',
|
||||
'平均风速',
|
||||
'3个叶片变桨角平均值',
|
||||
];
|
||||
const REPORT_PITCH_HEADER = '3个叶片变桨角平均值';
|
||||
|
||||
const REPORT_BORDER = {
|
||||
top: { style: 'thin', color: { argb: 'FF475569' } },
|
||||
@@ -1000,16 +1030,19 @@ function reportPitchAverage(row) {
|
||||
return values.every((value) => value !== '') ? values.reduce((sum, value) => sum + value, 0) / values.length : '';
|
||||
}
|
||||
|
||||
function reportDetailValues(row, fanId) {
|
||||
function reportDetailValues(row, fanId, includePitch) {
|
||||
const savedPitchAverage = reportNumber(row.pitch_angle_average);
|
||||
return [
|
||||
const values = [
|
||||
row.fan_id || fanId || '',
|
||||
row.time || '',
|
||||
reportNumber(row.active_power),
|
||||
reportNumber(row.generator_speed),
|
||||
reportNumber(row.wind_speed),
|
||||
savedPitchAverage !== '' ? savedPitchAverage : reportPitchAverage(row),
|
||||
];
|
||||
if (includePitch) {
|
||||
values.push(savedPitchAverage !== '' ? savedPitchAverage : reportPitchAverage(row));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function styleReportHeader(row) {
|
||||
@@ -1036,17 +1069,25 @@ function styleReportBody(sheet, startRow, endRow, numericColumns) {
|
||||
}
|
||||
|
||||
function addReportDetailSheet(workbook, name, rows, fanId) {
|
||||
const includePitch = rows.some((row) => (
|
||||
reportNumber(row.pitch_angle_average) !== '' || reportPitchAverage(row) !== ''
|
||||
));
|
||||
const headers = includePitch ? [...REPORT_DETAIL_HEADERS, REPORT_PITCH_HEADER] : REPORT_DETAIL_HEADERS;
|
||||
const sheet = workbook.addWorksheet(name, {
|
||||
views: [{ state: 'frozen', ySplit: 1 }],
|
||||
});
|
||||
sheet.columns = [
|
||||
{ width: 16 }, { width: 22 }, { width: 14 }, { width: 14 }, { width: 14 }, { width: 23 },
|
||||
{ width: 16 }, { width: 22 }, { width: 14 }, { width: 14 }, { width: 14 },
|
||||
...(includePitch ? [{ width: 23 }] : []),
|
||||
];
|
||||
sheet.addRow(REPORT_DETAIL_HEADERS);
|
||||
rows.forEach((row) => sheet.addRow(reportDetailValues(row, fanId)));
|
||||
sheet.addRow(headers);
|
||||
rows.forEach((row) => sheet.addRow(reportDetailValues(row, fanId, includePitch)));
|
||||
styleReportHeader(sheet.getRow(1));
|
||||
styleReportBody(sheet, 2, Math.max(2, rows.length + 1), [3, 4, 5, 6]);
|
||||
sheet.autoFilter = `A1:F${Math.max(1, rows.length + 1)}`;
|
||||
// 对每个数据单元格设置样式会使 ExcelJS 为大量行保留大量对象,容易耗尽浏览器内存。
|
||||
for (const columnNumber of includePitch ? [3, 4, 5, 6] : [3, 4, 5]) {
|
||||
sheet.getColumn(columnNumber).numFmt = '0.0000';
|
||||
}
|
||||
sheet.autoFilter = `A1:${includePitch ? 'F' : 'E'}${Math.max(1, rows.length + 1)}`;
|
||||
return sheet;
|
||||
}
|
||||
|
||||
@@ -1067,14 +1108,15 @@ function addRawReportSheet(workbook, { headers, rows }) {
|
||||
});
|
||||
sheet.addRow(headers);
|
||||
rows.forEach((row) => sheet.addRow(row));
|
||||
// 列宽只需参考样本即可,避免大文件导出时重复扫描全部单元格。
|
||||
const widthSampleRows = rows.slice(0, 1000);
|
||||
sheet.columns = headers.map((header, index) => {
|
||||
const maxLength = rows.reduce((max, row) => (
|
||||
const maxLength = widthSampleRows.reduce((max, row) => (
|
||||
Math.max(max, String(row[index] ?? '').length)
|
||||
), String(header ?? '').length);
|
||||
return { width: Math.min(32, Math.max(12, maxLength + 2)) };
|
||||
});
|
||||
styleReportHeader(sheet.getRow(1));
|
||||
styleReportBody(sheet, 2, Math.max(2, rows.length + 1), []);
|
||||
sheet.autoFilter = `A1:${excelColumnName(Math.max(0, headers.length - 1))}${Math.max(1, rows.length + 1)}`;
|
||||
return sheet;
|
||||
}
|
||||
@@ -1086,7 +1128,6 @@ async function exportPowerCurveReport({
|
||||
reportRows,
|
||||
chartRenderSnapshot,
|
||||
}) {
|
||||
const { default: ExcelJS } = await import('exceljs');
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
workbook.creator = '风电功率计算平台';
|
||||
workbook.created = new Date();
|
||||
@@ -1157,6 +1198,8 @@ async function exportPowerCurveReport({
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
function PowerCurveChart({
|
||||
points,
|
||||
scatterPoints,
|
||||
@@ -1218,7 +1261,7 @@ function PowerCurveChart({
|
||||
const yAxisSize = Math.max(64, Math.round(screenTypography.tick * 3.2 + 16));
|
||||
const axisLabelSize = screenTypography.axis + 18;
|
||||
const showScatterOnChart = activeChartOptions.show_scatter || editMode;
|
||||
chartSnapshotRef.current = async () => {
|
||||
chartSnapshotRef.current = async (captureFrame = true) => {
|
||||
const snapshot = {
|
||||
actualCurve: visibleActualCurve,
|
||||
designCurve: visibleDesignCurve,
|
||||
@@ -1231,6 +1274,9 @@ function PowerCurveChart({
|
||||
options: activeChartOptions,
|
||||
scales: plotRef.current?.scales,
|
||||
};
|
||||
if (!captureFrame) {
|
||||
return snapshot;
|
||||
}
|
||||
try {
|
||||
const capturedImage = await captureChartFrame(frameRef.current);
|
||||
return capturedImage ? { ...snapshot, ...capturedImage } : snapshot;
|
||||
@@ -1244,6 +1290,8 @@ function PowerCurveChart({
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || (!visibleActualCurve.length && !visibleDesignCurve.length)) return undefined;
|
||||
|
||||
const transientErasedPointIds = new Set();
|
||||
let redrawFrameId = 0;
|
||||
const sortedCurve = [...visibleActualCurve].sort((left, right) => left.wind_speed - right.wind_speed);
|
||||
const sortedDesign = [...visibleDesignCurve].sort((left, right) => left.wind_speed - right.wind_speed);
|
||||
const actualByWindSpeed = new Map(
|
||||
@@ -1372,6 +1420,9 @@ function PowerCurveChart({
|
||||
if (showScatterOnChart) {
|
||||
ctx.fillStyle = hexToRgba(activeChartOptions.scatter_color, activeChartOptions.scatter_opacity);
|
||||
for (const point of validScatter) {
|
||||
if (transientErasedPointIds.has(point.point_id)) {
|
||||
continue;
|
||||
}
|
||||
const x = u.valToPos(point.wind_speed, 'x', true);
|
||||
const y = u.valToPos(point.active_power, 'y', true);
|
||||
if (Number.isFinite(x) && Number.isFinite(y)) {
|
||||
@@ -1437,6 +1488,16 @@ function PowerCurveChart({
|
||||
plotRef.current = chart;
|
||||
setSeriesVisibility({ actual: true, design: true });
|
||||
|
||||
const requestChartRedraw = () => {
|
||||
if (redrawFrameId) {
|
||||
return;
|
||||
}
|
||||
redrawFrameId = window.requestAnimationFrame(() => {
|
||||
redrawFrameId = 0;
|
||||
chart.redraw(true, false);
|
||||
});
|
||||
};
|
||||
|
||||
const hideBrush = () => {
|
||||
setBrush((prev) => ({ ...prev, visible: false, dragging: false }));
|
||||
};
|
||||
@@ -1449,6 +1510,7 @@ function PowerCurveChart({
|
||||
}
|
||||
|
||||
let dragging = false;
|
||||
const pendingPoints = new Map();
|
||||
const updateBrush = (event, nextDragging = dragging) => {
|
||||
if (!frameRef.current) return;
|
||||
const rect = frameRef.current.getBoundingClientRect();
|
||||
@@ -1484,16 +1546,32 @@ function PowerCurveChart({
|
||||
if (!hitPoints.length) {
|
||||
return;
|
||||
}
|
||||
for (const point of hitPoints) {
|
||||
const pointId = point.point_id || `${point.wind_speed}_${point.active_power}`;
|
||||
pendingPoints.set(pointId, point);
|
||||
if (editTool === EDIT_TOOL_ERASE) {
|
||||
transientErasedPointIds.add(pointId);
|
||||
}
|
||||
}
|
||||
if (editTool === EDIT_TOOL_ERASE) {
|
||||
onErasePoints?.(hitPoints);
|
||||
requestChartRedraw();
|
||||
}
|
||||
};
|
||||
const applyPendingPoints = () => {
|
||||
if (!pendingPoints.size) return;
|
||||
const points = Array.from(pendingPoints.values());
|
||||
pendingPoints.clear();
|
||||
if (editTool === EDIT_TOOL_ERASE) {
|
||||
onErasePoints?.(points);
|
||||
} else {
|
||||
onRestorePoints?.(hitPoints);
|
||||
onRestorePoints?.(points);
|
||||
}
|
||||
};
|
||||
const handlePointerDown = (event) => {
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
dragging = true;
|
||||
chart.over.setPointerCapture?.(event.pointerId);
|
||||
updateBrush(event, true);
|
||||
applyToolAt(event);
|
||||
};
|
||||
@@ -1506,6 +1584,11 @@ function PowerCurveChart({
|
||||
const handlePointerUp = (event) => {
|
||||
updateBrush(event, false);
|
||||
dragging = false;
|
||||
applyToolAt(event);
|
||||
applyPendingPoints();
|
||||
if (chart.over.hasPointerCapture?.(event.pointerId)) {
|
||||
chart.over.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
};
|
||||
|
||||
chart.over.addEventListener('pointerdown', handlePointerDown);
|
||||
@@ -1518,6 +1601,10 @@ function PowerCurveChart({
|
||||
chart.over.removeEventListener('pointermove', handlePointerMove);
|
||||
chart.over.removeEventListener('pointerleave', hideBrush);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
if (redrawFrameId) {
|
||||
window.cancelAnimationFrame(redrawFrameId);
|
||||
}
|
||||
pendingPoints.clear();
|
||||
if (plotRef.current === chart) plotRef.current = null;
|
||||
chart.destroy();
|
||||
};
|
||||
@@ -1626,6 +1713,8 @@ export default function HomePage() {
|
||||
const [savingChartOptions, setSavingChartOptions] = useState(false);
|
||||
const [chartSettingsMessage, setChartSettingsMessage] = useState('');
|
||||
const [exportingReport, setExportingReport] = useState(false);
|
||||
const [reportExportProgress, setReportExportProgress] = useState('');
|
||||
const [reportExportNotice, setReportExportNotice] = useState('');
|
||||
const chartExportRef = useRef(() => false);
|
||||
const chartSnapshotRef = useRef(() => null);
|
||||
const [schemes, setSchemes] = useState(DEFAULT_SCHEMES);
|
||||
@@ -1633,12 +1722,14 @@ export default function HomePage() {
|
||||
const [schemeDescription, setSchemeDescription] = useState(DEFAULT_SCHEMES[0].description);
|
||||
const [savingScheme, setSavingScheme] = useState(false);
|
||||
const [schemeMessage, setSchemeMessage] = useState('');
|
||||
const [schemeOneParams, setSchemeOneParams] = useState(DEFAULT_SCHEME_ONE_PARAMS);
|
||||
const [schemeTwoParams, setSchemeTwoParams] = useState(DEFAULT_SCHEME_TWO_PARAMS);
|
||||
|
||||
const headers = files[0]?.headers || [];
|
||||
const missingFields = REQUIRED_FIELDS.filter((field) => !mapping[field.key]);
|
||||
const selectedScheme = schemes.find((scheme) => scheme.id === selectedSchemeId) || schemes[0];
|
||||
const isSchemeTwo = selectedSchemeId === SCHEME_TWO_ID;
|
||||
const mappingFields = isSchemeTwo ? REQUIRED_FIELDS : REQUIRED_FIELDS.slice(0, 5);
|
||||
const missingFields = mappingFields.filter((field) => !mapping[field.key]);
|
||||
const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : [];
|
||||
const selectedBins = selectedFan && result?.bins ? result.bins[selectedFan] || [] : [];
|
||||
const selectedScatterRaw = selectedFan && result?.scatter_points
|
||||
@@ -1702,14 +1793,6 @@ export default function HomePage() {
|
||||
() => hasMatchingHeaderSequence(files),
|
||||
[files],
|
||||
);
|
||||
const rawReportSourceFiles = useMemo(
|
||||
() => findRawSourceFiles(files, mapping, selectedFan),
|
||||
[files, mapping, selectedFan],
|
||||
);
|
||||
const selectedRawReportData = useMemo(() => {
|
||||
if (!hasConsistentRawHeaders || rawReportSourceFiles.length !== 1) return null;
|
||||
return buildRawReportData(rawReportSourceFiles[0], mapping, selectedFan);
|
||||
}, [hasConsistentRawHeaders, mapping, rawReportSourceFiles, selectedFan]);
|
||||
const selectedEstimatedParams = selectedFan && result?.estimated_params
|
||||
? result.estimated_params[selectedFan]
|
||||
: null;
|
||||
@@ -1924,48 +2007,68 @@ export default function HomePage() {
|
||||
() => buildReportCurveRows(effectiveScatter, reportDesignCurve),
|
||||
[effectiveScatter, reportDesignCurve],
|
||||
);
|
||||
const reportExportDisabledReason = files.length && !hasConsistentRawHeaders
|
||||
? '同一批上传文件的列头和列顺序必须完全一致'
|
||||
: files.length && rawReportSourceFiles.length !== 1
|
||||
? '完整报告要求当前风机对应唯一一份上传 Excel 文件'
|
||||
: files.length && (!selectedRawReportData || !selectedRawReportData.rows.length)
|
||||
? '当前风机没有可导出的原始数据,或未识别到风速列'
|
||||
const reportExportDisabledReason = !selectedFan
|
||||
? '请先完成计算并选择要导出的风机'
|
||||
: isEditMode
|
||||
? '请先退出曲线编辑模式,再导出完整报告'
|
||||
: !reportDesignCurve.length
|
||||
? '请先填写或上传设计功率曲线'
|
||||
: !result?.job_id
|
||||
? '当前计算结果已失效,请重新计算后导出报告'
|
||||
: files.length && !hasConsistentRawHeaders
|
||||
? '同一批上传文件的列头和列顺序必须完全一致'
|
||||
: !reportCurveRows.length
|
||||
? '当前设计功率曲线与有效数据未形成可导出的功率曲线点'
|
||||
: '';
|
||||
const canExportFullReport = Boolean(
|
||||
selectedFan && selectedRawReportData?.rows.length && reportCurveRows.length && !isEditMode &&
|
||||
!reportExportDisabledReason,
|
||||
);
|
||||
const handleExportFullReport = useCallback(async () => {
|
||||
if (!canExportFullReport || exportingReport) return;
|
||||
if (exportingReport) return;
|
||||
if (!canExportFullReport) {
|
||||
setReportExportNotice(reportExportDisabledReason || '当前条件不满足,无法导出完整报告');
|
||||
return;
|
||||
}
|
||||
setExportingReport(true);
|
||||
setReportExportProgress('正在服务器生成完整报告');
|
||||
setError('');
|
||||
try {
|
||||
const chartRenderSnapshot = await chartSnapshotRef.current?.();
|
||||
const chartRenderSnapshot = await chartSnapshotRef.current?.(false);
|
||||
if (!chartRenderSnapshot) {
|
||||
throw new Error('图表尚未完成绘制,请稍后重试');
|
||||
}
|
||||
await exportPowerCurveReport({
|
||||
fanId: selectedFan,
|
||||
rawReportData: selectedRawReportData,
|
||||
effectiveRows: effectiveScatter,
|
||||
reportRows: reportCurveRows,
|
||||
chartRenderSnapshot,
|
||||
const chartImageData = renderPowerCurvePng(chartRenderSnapshot);
|
||||
const blob = await downloadWindReport(result.job_id, {
|
||||
fan_id: selectedFan,
|
||||
effective_rows: effectiveScatter,
|
||||
report_rows: reportCurveRows,
|
||||
chart_image: chartImageData || '',
|
||||
});
|
||||
downloadReportBlob(blob, selectedFan);
|
||||
} catch (err) {
|
||||
setError(err.message || '导出完整报告失败');
|
||||
} finally {
|
||||
const message = err.message || '导出完整报告失败';
|
||||
setError(message);
|
||||
setReportExportNotice(message);
|
||||
setExportingReport(false);
|
||||
setReportExportProgress('');
|
||||
}
|
||||
}, [
|
||||
canExportFullReport,
|
||||
effectiveScatter,
|
||||
exportingReport,
|
||||
reportExportDisabledReason,
|
||||
reportCurveRows,
|
||||
rawReportSourceFiles,
|
||||
selectedRawReportData,
|
||||
selectedFan,
|
||||
result?.job_id,
|
||||
]);
|
||||
|
||||
const handleCancelReportExport = useCallback(() => {
|
||||
setExportingReport(false);
|
||||
setReportExportProgress('');
|
||||
setReportExportNotice('已取消完整报告导出');
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
@@ -1981,6 +2084,8 @@ export default function HomePage() {
|
||||
setSchemes(loadedSchemes);
|
||||
setSelectedSchemeId(defaultScheme.id);
|
||||
setSchemeDescription(defaultScheme.description || '');
|
||||
const schemeOne = loadedSchemes.find((scheme) => scheme.id === DEFAULT_SCHEME_ID);
|
||||
setSchemeOneParams(schemeOneParamsToState(schemeOne?.parameters));
|
||||
const schemeTwo = loadedSchemes.find((scheme) => scheme.id === SCHEME_TWO_ID);
|
||||
setSchemeTwoParams(schemeParamsToState(schemeTwo?.parameters));
|
||||
} catch (err) {
|
||||
@@ -2000,7 +2105,9 @@ export default function HomePage() {
|
||||
const nextScheme = schemes.find((scheme) => scheme.id === value);
|
||||
setSelectedSchemeId(value);
|
||||
setSchemeDescription(nextScheme?.description || '');
|
||||
if (value === SCHEME_TWO_ID) {
|
||||
if (value === DEFAULT_SCHEME_ID) {
|
||||
setSchemeOneParams(schemeOneParamsToState(nextScheme?.parameters));
|
||||
} else if (value === SCHEME_TWO_ID) {
|
||||
setSchemeTwoParams(schemeParamsToState(nextScheme?.parameters));
|
||||
}
|
||||
setSchemeMessage('');
|
||||
@@ -2013,6 +2120,13 @@ export default function HomePage() {
|
||||
}));
|
||||
}
|
||||
|
||||
function handleSchemeOneParamChange(field, value) {
|
||||
setSchemeOneParams((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
}
|
||||
|
||||
async function handleSaveSchemeDescription() {
|
||||
if (!selectedScheme) return;
|
||||
|
||||
@@ -2020,9 +2134,26 @@ export default function HomePage() {
|
||||
setError('');
|
||||
setSchemeMessage('');
|
||||
try {
|
||||
const schemeOneValues = Object.fromEntries(Object.entries(schemeOneParams).map(([field, value]) => [
|
||||
field,
|
||||
normalizeNumber(value),
|
||||
]));
|
||||
const gridConnectedSpeed = normalizeNumber(schemeTwoParams.grid_connected_speed);
|
||||
const ratedGeneratorSpeed = normalizeNumber(schemeTwoParams.rated_generator_speed);
|
||||
const ratedPower = normalizeNumber(schemeTwoParams.rated_power);
|
||||
if (selectedScheme.id === DEFAULT_SCHEME_ID &&
|
||||
(!Number.isFinite(schemeOneValues.rated_power) || schemeOneValues.rated_power <= 0 ||
|
||||
!Number.isFinite(schemeOneValues.rated_wind_speed) || schemeOneValues.rated_wind_speed <= 0 ||
|
||||
!Number.isFinite(schemeOneValues.power_step) || schemeOneValues.power_step <= 0 ||
|
||||
!Number.isFinite(schemeOneValues.cleaning_wind_speed_step) || schemeOneValues.cleaning_wind_speed_step <= 0 ||
|
||||
!Number.isFinite(schemeOneValues.rotor_radius) || schemeOneValues.rotor_radius <= 0 ||
|
||||
!Number.isFinite(schemeOneValues.gearbox_ratio) || schemeOneValues.gearbox_ratio <= 0 ||
|
||||
['wind_speed_change_threshold', 'iqr_lower_multiplier', 'iqr_upper_multiplier',
|
||||
'minimum_generator_speed', 'generator_speed_k'].some((field) => (
|
||||
!Number.isFinite(schemeOneValues[field]) || schemeOneValues[field] < 0
|
||||
)))) {
|
||||
throw new Error('方案一参数必须为合法数值,额定与步长参数、叶轮半径和传动比必须大于 0');
|
||||
}
|
||||
if (selectedScheme.id === SCHEME_TWO_ID &&
|
||||
(!Number.isFinite(gridConnectedSpeed) || gridConnectedSpeed <= 0 ||
|
||||
!Number.isFinite(ratedGeneratorSpeed) || ratedGeneratorSpeed <= 0 ||
|
||||
@@ -2031,6 +2162,7 @@ export default function HomePage() {
|
||||
}
|
||||
const data = await saveWindSchemeDescription(selectedScheme.id, {
|
||||
description: schemeDescription,
|
||||
...(selectedScheme.id === DEFAULT_SCHEME_ID ? { parameters: schemeOneValues } : {}),
|
||||
...(selectedScheme.id === SCHEME_TWO_ID ? {
|
||||
parameters: {
|
||||
grid_connected_speed: gridConnectedSpeed,
|
||||
@@ -2044,7 +2176,9 @@ export default function HomePage() {
|
||||
scheme.id === savedScheme.id ? { ...scheme, ...savedScheme } : scheme
|
||||
)));
|
||||
setSchemeDescription(savedScheme.description || '');
|
||||
if (savedScheme.id === SCHEME_TWO_ID) {
|
||||
if (savedScheme.id === DEFAULT_SCHEME_ID) {
|
||||
setSchemeOneParams(schemeOneParamsToState(savedScheme.parameters));
|
||||
} else if (savedScheme.id === SCHEME_TWO_ID) {
|
||||
setSchemeTwoParams(schemeParamsToState(savedScheme.parameters));
|
||||
}
|
||||
setSchemeMessage('已保存');
|
||||
@@ -2142,6 +2276,26 @@ export default function HomePage() {
|
||||
setError('请先上传文件并完成所有字段映射');
|
||||
return;
|
||||
}
|
||||
const schemeOneValues = Object.fromEntries(Object.entries(schemeOneParams).map(([field, value]) => [
|
||||
field,
|
||||
normalizeNumber(value),
|
||||
]));
|
||||
const schemeOneInvalid =
|
||||
!Number.isFinite(schemeOneValues.rated_power) || schemeOneValues.rated_power <= 0 ||
|
||||
!Number.isFinite(schemeOneValues.rated_wind_speed) || schemeOneValues.rated_wind_speed <= 0 ||
|
||||
!Number.isFinite(schemeOneValues.power_step) || schemeOneValues.power_step <= 0 ||
|
||||
!Number.isFinite(schemeOneValues.cleaning_wind_speed_step) ||
|
||||
schemeOneValues.cleaning_wind_speed_step <= 0 ||
|
||||
!Number.isFinite(schemeOneValues.rotor_radius) || schemeOneValues.rotor_radius <= 0 ||
|
||||
!Number.isFinite(schemeOneValues.gearbox_ratio) || schemeOneValues.gearbox_ratio <= 0 ||
|
||||
['wind_speed_change_threshold', 'iqr_lower_multiplier', 'iqr_upper_multiplier',
|
||||
'minimum_generator_speed', 'generator_speed_k'].some((field) => (
|
||||
!Number.isFinite(schemeOneValues[field]) || schemeOneValues[field] < 0
|
||||
));
|
||||
if (!isSchemeTwo && schemeOneInvalid) {
|
||||
setError('方案一参数必须为合法数值,额定与步长参数、叶轮半径和传动比必须大于 0');
|
||||
return;
|
||||
}
|
||||
const gridConnectedSpeed = normalizeNumber(schemeTwoParams.grid_connected_speed);
|
||||
const ratedGeneratorSpeed = normalizeNumber(schemeTwoParams.rated_generator_speed);
|
||||
const ratedPower = normalizeNumber(schemeTwoParams.rated_power);
|
||||
@@ -2166,13 +2320,14 @@ export default function HomePage() {
|
||||
setProgress('正在准备标准化数据');
|
||||
|
||||
try {
|
||||
const { rows } = buildStandardRows(files, mapping);
|
||||
const { rows, rawRows } = buildStandardRows(files, mapping);
|
||||
const start = await startWindJob({
|
||||
files: files.map((file) => ({
|
||||
file_name: file.file_name,
|
||||
row_count: file.row_count,
|
||||
})),
|
||||
mapping,
|
||||
raw_headers: files[0]?.raw_headers || files[0]?.headers || [],
|
||||
});
|
||||
jobId = start.job_id;
|
||||
|
||||
@@ -2184,6 +2339,7 @@ export default function HomePage() {
|
||||
job_id: jobId,
|
||||
chunk_index: index,
|
||||
rows: chunkRows,
|
||||
raw_rows: rawRows.slice(index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2191,15 +2347,9 @@ export default function HomePage() {
|
||||
const calculation = await finishWindJob({
|
||||
job_id: jobId,
|
||||
options: {
|
||||
power_step: 5,
|
||||
cleaning_wind_speed_step: 0.25,
|
||||
curve_wind_speed_step: 0.5,
|
||||
wind_speed_change_threshold: 1,
|
||||
iqr_lower_multiplier: 1.8,
|
||||
iqr_upper_multiplier: 2,
|
||||
minimum_generator_speed: 1,
|
||||
generator_speed_k: 0.9,
|
||||
scheme_id: selectedSchemeId,
|
||||
...(!isSchemeTwo ? schemeOneValues : {}),
|
||||
...(isSchemeTwo ? {
|
||||
grid_connected_speed: gridConnectedSpeed,
|
||||
rated_generator_speed: ratedGeneratorSpeed,
|
||||
@@ -2207,7 +2357,7 @@ export default function HomePage() {
|
||||
} : {}),
|
||||
},
|
||||
});
|
||||
setResult(calculation);
|
||||
setResult({ ...calculation, job_id: jobId });
|
||||
setSelectedFan(calculation.fans?.[0] || '');
|
||||
setProgress('计算完成');
|
||||
} catch (err) {
|
||||
@@ -2218,7 +2368,11 @@ export default function HomePage() {
|
||||
// 失败任务清理失败不影响用户看到主错误。
|
||||
}
|
||||
}
|
||||
setError(err.message || '计算失败');
|
||||
const message = err.message || '计算失败';
|
||||
setError(message);
|
||||
if (message.includes('服务器正在处理数据')) {
|
||||
setReportExportNotice(message);
|
||||
}
|
||||
setProgress('');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
@@ -2248,6 +2402,19 @@ export default function HomePage() {
|
||||
|
||||
{error && <div className="alert error">{error}</div>}
|
||||
{progress && <div className="alert info">{progress}</div>}
|
||||
{reportExportNotice && (
|
||||
<div className="dialogBackdrop" role="presentation">
|
||||
<section className="appDialog" role="dialog" aria-modal="true" aria-labelledby="report-export-notice-title">
|
||||
<h2 id="report-export-notice-title">提示</h2>
|
||||
<p>{reportExportNotice}</p>
|
||||
<div className="appDialogActions">
|
||||
<button className="primaryButton" type="button" onClick={() => setReportExportNotice('')}>
|
||||
我知道了
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="panel schemePanel">
|
||||
<div className="panelHeader">
|
||||
@@ -2283,6 +2450,29 @@ export default function HomePage() {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{!isSchemeTwo && (
|
||||
<>
|
||||
<p className="schemeParamHint">
|
||||
叶尖速比 = 发电机转速 × 3.14 × 齿轮箱传动比 × 叶轮半径 × 30 ÷ 风速
|
||||
</p>
|
||||
<div className="schemeParamGrid">
|
||||
{SCHEME_ONE_PARAM_FIELDS.map((field) => (
|
||||
<label className="fieldControl" key={field.key}>
|
||||
<span>{field.label}{field.unit ? ` (${field.unit})` : ''}</span>
|
||||
<input
|
||||
type="number"
|
||||
min={field.min}
|
||||
step={field.step}
|
||||
value={schemeOneParams[field.key]}
|
||||
onChange={(event) => handleSchemeOneParamChange(field.key, event.target.value)}
|
||||
disabled={submitting || savingScheme}
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isSchemeTwo && (
|
||||
<div className="schemeParamGrid">
|
||||
<label className="fieldControl">
|
||||
@@ -2457,7 +2647,7 @@ export default function HomePage() {
|
||||
<div className="emptyState">上传文件后可指定列头对应关系。</div>
|
||||
) : (
|
||||
<div className="mappingGrid">
|
||||
{REQUIRED_FIELDS.map((field) => (
|
||||
{mappingFields.map((field) => (
|
||||
<label className="fieldControl" key={field.key}>
|
||||
<span>{field.label}</span>
|
||||
<select
|
||||
@@ -2594,11 +2784,16 @@ export default function HomePage() {
|
||||
className="secondaryButton"
|
||||
type="button"
|
||||
onClick={handleExportFullReport}
|
||||
disabled={!canExportFullReport || exportingReport}
|
||||
disabled={exportingReport}
|
||||
title={reportExportDisabledReason || '导出当前风机的完整功率曲线报告'}
|
||||
>
|
||||
{exportingReport ? '导出报告中...' : '导出完整报告'}
|
||||
{exportingReport ? (reportExportProgress || '导出报告中...') : '导出完整报告'}
|
||||
</button>
|
||||
{exportingReport && (
|
||||
<button className="secondaryButton" type="button" onClick={handleCancelReportExport}>
|
||||
取消导出
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="secondaryButton"
|
||||
type="button"
|
||||
|
||||
@@ -141,3 +141,24 @@ export function deleteWindJob(jobId) {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/** 服务端生成完整报告并以文件流返回。 */
|
||||
export async function downloadWindReport(jobId, payload) {
|
||||
const resp = await fetch(`${BASE_URL}/wind/jobs/${encodeURIComponent(jobId)}/report`, {
|
||||
method: 'POST',
|
||||
cache: 'no-store',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
throw new Error(json.msg || '完整报告生成失败');
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) throw new Error('完整报告生成失败,请稍后重试');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return resp.blob();
|
||||
}
|
||||
|
||||
@@ -63,8 +63,11 @@ step "3/5 构建前端 ..."
|
||||
|
||||
# 4. 整理 runtime 目录
|
||||
step "4/5 整理 runtime/wind_power/ ..."
|
||||
rm -rf "${RUNTIME_DIR}"
|
||||
mkdir -p "${RUNTIME_DIR}"/{config,web,libs,logs}
|
||||
# data/ 是服务端运行期持久化目录,不能随打包清理,否则保存的方案和图表配置会丢失。
|
||||
mkdir -p "${RUNTIME_DIR}"/{data,uploads,logs}
|
||||
rm -rf "${RUNTIME_DIR}/config" "${RUNTIME_DIR}/web" "${RUNTIME_DIR}/libs"
|
||||
rm -f "${RUNTIME_DIR}/wind_server"
|
||||
mkdir -p "${RUNTIME_DIR}"/{config,web,libs}
|
||||
|
||||
cp "${PKG_BUILD}/wind_server" "${RUNTIME_DIR}/"
|
||||
cp -r "${ROOT_DIR}"/backend/config/* "${RUNTIME_DIR}/config/"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
custom: ["paypal.me/xlsxwriter"]
|
||||
@@ -0,0 +1,85 @@
|
||||
# libxlsxwriter: Reporting Bugs
|
||||
|
||||
Here are some tips on reporting bugs in `libxlsxwriter`.
|
||||
|
||||
### Upgrade to the latest version of the library
|
||||
|
||||
Upgrade to the latest version of the library since the bug you are reporting
|
||||
may already be fixed.
|
||||
|
||||
Check the [Changes][changes] section of the documentation to see what has
|
||||
changed in the latest versions.
|
||||
|
||||
[changes]: http://libxlsxwriter.github.io/changes.html
|
||||
|
||||
You can check which version of `libxlsxwriter` that you are using by checking
|
||||
the `xlsxwriter.h` header file or by adding the following to your program:
|
||||
|
||||
```C
|
||||
#include <stdio.h>
|
||||
#include "xlsxwriter.h"
|
||||
|
||||
int main() {
|
||||
|
||||
printf("Libxlsxwriter version = %s\n", lxw_version());
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Read the documentation
|
||||
|
||||
Read or search the `libxlsxwriter` [documentation][docs] to see if the issue
|
||||
you are encountering is already explained.
|
||||
|
||||
[docs]: http://libxlsxwriter.github.io/index.html
|
||||
|
||||
### Look at the example programs
|
||||
|
||||
There are many [examples programs][examples] in the distribution. Try to
|
||||
identify an example program that corresponds to your query and adapt it to use
|
||||
as a bug report.
|
||||
|
||||
[examples]: http://libxlsxwriter.github.io/examples.html
|
||||
|
||||
|
||||
### Tips for submitting a bug report
|
||||
|
||||
1. Describe the problem as clearly and as concisely as possible.
|
||||
2. Include a sample program. This is probably the most important step.
|
||||
It is generally easier to describe a problem in code than in written
|
||||
prose.
|
||||
3. The sample program should be as small as possible to demonstrate the
|
||||
problem. Don't copy and paste large non-relevant sections of your
|
||||
program.
|
||||
|
||||
A sample bug report is shown below. This format helps analyze and respond to
|
||||
the bug report more quickly.
|
||||
|
||||
|
||||
> Subject: Issue with SOMETHING
|
||||
>
|
||||
> Greetings,
|
||||
>
|
||||
> I am using libxlsxwriter to do SOMETHING but it appears to do SOMETHING ELSE.
|
||||
>
|
||||
> I am using CC version X.Y.Z, OS = uname and libxlsxwriter x.y.z.
|
||||
>
|
||||
> Here is some code that demonstrates the problem:
|
||||
>
|
||||
>
|
||||
>```C
|
||||
>#include "xlsxwriter.h"
|
||||
>
|
||||
>int main() {
|
||||
>
|
||||
> lxw_workbook *workbook = workbook_new("bug_report.xlsx");
|
||||
> lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
|
||||
>
|
||||
> worksheet_write_string(worksheet, 0, 0, "Hello", NULL);
|
||||
> worksheet_write_number(worksheet, 1, 0, 123, NULL);
|
||||
>
|
||||
> return workbook_close(workbook);
|
||||
>}
|
||||
>```
|
||||
>
|
||||
@@ -0,0 +1,130 @@
|
||||
# libxlsxwriter: Submitting Pull Requests
|
||||
|
||||
# Pull Requests and Contributing to Libxlsxwriter
|
||||
|
||||
All patches and pull requests are welcome but in general you should start with
|
||||
an issue tracker to describe what you intend to do before you do it.
|
||||
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Pull requests and new feature proposals must start with an [issue
|
||||
tracker][issues]. This serves as the focal point for the design discussion.
|
||||
2. Describe what you plan to do. If there are API changes add some code
|
||||
example to demonstrate them.
|
||||
3. Fork the repository.
|
||||
4. Run all the tests to make sure the current code works on your system using
|
||||
`make test`. See the [Running the Test Suite][tests] section of the docs
|
||||
for instructions.
|
||||
5. Create a feature branch for your new feature.
|
||||
|
||||
|
||||
[tests]: http://libxlsxwriter.github.io/running_the_tests.html
|
||||
|
||||
### Code Style
|
||||
|
||||
The code style is mainly K&R style with 4 space indents.
|
||||
|
||||
The author uses GNU indent (`gindent`) 2.2.10 with the following options:
|
||||
|
||||
```
|
||||
--braces-on-if-line
|
||||
--braces-on-struct-decl-line
|
||||
--case-indentation 4
|
||||
--continue-at-parentheses
|
||||
--declaration-comment-column 0
|
||||
--format-first-column-comments
|
||||
--honour-newlines
|
||||
--ignore-profile
|
||||
--indent-label 0
|
||||
--indent-level 4
|
||||
--no-space-after-function-call-names
|
||||
--no-tabs
|
||||
--swallow-optional-blank-lines
|
||||
```
|
||||
|
||||
The [indent configuration file][indentpro] is available in the repo. The code
|
||||
can be indented automatically if the same version of `gindent` is used with
|
||||
the following make command:
|
||||
|
||||
```shell
|
||||
make indent
|
||||
```
|
||||
|
||||
Note, make sure you have backed up your files or added them to the index
|
||||
before running this command.
|
||||
|
||||
In general follow the existing style in the code.
|
||||
|
||||
[indentpro]: https://github.com/jmcnamara/libxlsxwriter/blob/master/.indent.pro
|
||||
|
||||
### Writing and Running Tests
|
||||
|
||||
Any significant features should be accompanied by a test. See the `test`
|
||||
directory and the [Running the Test Suite][tests] section of the docs for
|
||||
details of the test setup.
|
||||
|
||||
The tests can be run as follows:
|
||||
|
||||
```shell
|
||||
make test
|
||||
```
|
||||
Same as:
|
||||
|
||||
```shell
|
||||
make test_unit
|
||||
make test_functional
|
||||
```
|
||||
|
||||
The functional tests require the Python module [pytest][pytest] as a test runner.
|
||||
|
||||
If you have `valgrind` installed you can use the test suite to check for memory leaks:
|
||||
|
||||
```shell
|
||||
make test_valgrind
|
||||
```
|
||||
|
||||
When you push your changes they will also be tested automatically using
|
||||
[GitHub Actions][actions].
|
||||
|
||||
[actions]: https://github.com/jmcnamara/libxlsxwriter/actions
|
||||
[pytest]: http://pytest.org/
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
The `libxlsxwriter` documentation is written in Doxygen format in the header
|
||||
files and in additional `.dox` files in the `docs/src` directory of the
|
||||
repo. The documentation can be built as follows:
|
||||
|
||||
```shell
|
||||
make docs
|
||||
open docs/html/index.html
|
||||
```
|
||||
|
||||
|
||||
### Example programs
|
||||
|
||||
If applicable add an example program to the `examples` directory. Example
|
||||
files can be built using:
|
||||
|
||||
```shell
|
||||
make docs
|
||||
```
|
||||
|
||||
### Copyright and License
|
||||
|
||||
Copyright remains with the original author. Do not include additional
|
||||
copyright claims or Licensing requirements. GitHub and the `git` repository
|
||||
will record your contribution and it will be acknowledged it in the Changes
|
||||
file.
|
||||
|
||||
|
||||
### Submitting the Pull Request
|
||||
|
||||
If your change involves several incremental `git` commits then `rebase` or
|
||||
`squash` them onto another branch so that the Pull Request is a single commit
|
||||
or a small number of logical commits.
|
||||
|
||||
Push your changes to GitHub and submit the Pull Request with a hash link to
|
||||
the to the Issue tracker that was opened above.
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Build with CMake
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name:
|
||||
Cmake
|
||||
strategy:
|
||||
matrix:
|
||||
cc: [gcc, clang]
|
||||
cmake_flags: ["",
|
||||
"-DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_DTOA_LIBRARY=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_MEM_FILE=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_NO_MD5=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_OPENSSL_MD5=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_STANDARD_TMPFILE=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_SYSTEM_MINIZIP=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_SYSTEM_MINIZIP=ON -DUSE_OPENSSL_MD5=ON -DBUILD_TESTS=ON"]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CC: ${{ matrix.cc }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest
|
||||
sudo apt-get -y install zlib1g-dev
|
||||
sudo apt-get -y install libminizip-dev
|
||||
sudo apt-get -y install libssl-dev
|
||||
|
||||
- name: Configure CMake
|
||||
working-directory: ${{ github.workspace }}/cmake
|
||||
run: cmake .. -DBUILD_TESTS=ON ${{ matrix.cmake_flags }} -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Build
|
||||
working-directory: ${{ github.workspace }}/cmake
|
||||
run: cmake --build . --config Release --parallel
|
||||
|
||||
- name: Test
|
||||
working-directory: ${{ github.workspace }}/cmake
|
||||
run: ctest -C Release -V
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Check code style
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name:
|
||||
Check code style
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get -y install indent
|
||||
sudo ln -s /usr/bin/indent /usr/bin/gindent
|
||||
|
||||
- name: Make indent
|
||||
run: |
|
||||
make indent
|
||||
git status | grep 'nothing to commit'
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Coverity Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [coverity]
|
||||
|
||||
jobs:
|
||||
coverity:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build third party libs to exclude them from scan
|
||||
run: make third_party
|
||||
|
||||
- uses: vapier/coverity-scan-action@v1
|
||||
with:
|
||||
project: libxlsxwriter
|
||||
email: ${{ secrets.COVERITY_SCAN_EMAIL }}
|
||||
token: ${{ secrets.COVERITY_SCAN_TOKEN }}
|
||||
command: make -C src libxlsxwriter.a
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Build with Make
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name:
|
||||
Make
|
||||
strategy:
|
||||
matrix:
|
||||
cc: [gcc, clang]
|
||||
make_flags: ["",
|
||||
"USE_STANDARD_TMPFILE=1",
|
||||
"USE_SYSTEM_MINIZIP=1",
|
||||
"USE_DTOA_LIBRARY=1",
|
||||
"USE_NO_MD5=1",
|
||||
"USE_OPENSSL_MD5=1",
|
||||
"USE_MEM_FILE=1"]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CC: ${{ matrix.cc }}
|
||||
CXX: ${{ matrix.cc }}
|
||||
CFLAGS: '-Werror'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest
|
||||
sudo apt-get -y install zlib1g-dev
|
||||
sudo apt-get -y install libminizip-dev
|
||||
sudo apt-get -y install libssl-dev
|
||||
sudo apt-get -y install valgrind
|
||||
|
||||
- name: make
|
||||
run: ${{ matrix.make_flags }} make V=1
|
||||
|
||||
- name: test unit
|
||||
run: ${{ matrix.make_flags }} make test_unit V=1
|
||||
|
||||
- name: test functional
|
||||
run: ${{ matrix.make_flags }} make test_functional V=1 -j
|
||||
|
||||
- name: test cpp
|
||||
run: ${{ matrix.make_flags }} make test_cpp V=1
|
||||
|
||||
- name: test examples
|
||||
run: ${{ matrix.make_flags }} make examples V=1
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Test for memory leaks
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name:
|
||||
Valgrind
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CC: gcc
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get -y install valgrind
|
||||
sudo apt-get -y install zlib1g-dev
|
||||
|
||||
- name: test valgrind
|
||||
run: make test_valgrind V=1 -j 2
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Cmake on Windows
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: CMake on Windows
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
cmake_flags: ["-DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_DTOA_LIBRARY=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_OPENSSL_MD5=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_SYSTEM_MINIZIP=ON -DBUILD_TESTS=ON",
|
||||
"-DUSE_SYSTEM_MINIZIP=ON -DUSE_OPENSSL_MD5=ON -DBUILD_TESTS=ON"]
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ${{env.GITHUB_WORKSPACE}}
|
||||
shell: cmd
|
||||
run: |
|
||||
vcpkg.exe install zlib:x64-windows minizip:x64-windows openssl:x64-windows
|
||||
vcpkg.exe integrate install
|
||||
pip install pytest
|
||||
|
||||
- name: Configure CMake
|
||||
working-directory: ${{env.GITHUB_WORKSPACE}}
|
||||
shell: cmd
|
||||
run: |
|
||||
cd cmake
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release ${{ matrix.cmake_flags }} -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -A x64
|
||||
|
||||
- name: Build
|
||||
working-directory: ${{env.GITHUB_WORKSPACE}}
|
||||
shell: cmd
|
||||
run: |
|
||||
cd cmake
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cmake --build . --config Release
|
||||
|
||||
- name: Test
|
||||
working-directory: ${{env.GITHUB_WORKSPACE}}
|
||||
shell: cmd
|
||||
run: |
|
||||
cd cmake
|
||||
copy test\functional\src\Release\*.exe test\functional\src
|
||||
pytest -v test/functional
|
||||
@@ -0,0 +1,62 @@
|
||||
*.a
|
||||
*.o
|
||||
*.so
|
||||
*.so.*
|
||||
*.to
|
||||
*.lo
|
||||
*.la
|
||||
*.dylib
|
||||
*.dll
|
||||
*.gcno
|
||||
*.gcda
|
||||
test_*
|
||||
!test_*.c
|
||||
!test_*.cpp
|
||||
!test_*.py
|
||||
*.tar.gz
|
||||
*~
|
||||
TAGS
|
||||
.#*
|
||||
*#
|
||||
~*xlsx
|
||||
*.xlsx
|
||||
*.bak
|
||||
!test/functional/xlsx_files/*.xlsx
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
.cproject
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
.DS_Store
|
||||
__pycache__
|
||||
.cache
|
||||
docs/html
|
||||
docs/latex
|
||||
.deps
|
||||
.dirstamp
|
||||
_temp.c
|
||||
examples/*
|
||||
!examples/*.c
|
||||
!examples/*.png
|
||||
!examples/Makefile
|
||||
!examples/vbaProject.bin
|
||||
cov-int
|
||||
libxlsxwriter-coverity.tgz
|
||||
build
|
||||
|
||||
third_party/zlib-1.2.8/configure.log
|
||||
third_party/zlib-1.2.8/contrib/minizip/miniunz
|
||||
third_party/zlib-1.2.8/contrib/minizip/minizip
|
||||
third_party/zlib-1.2.8/example
|
||||
third_party/zlib-1.2.8/examplesh
|
||||
third_party/zlib-1.2.8/minigzip
|
||||
third_party/zlib-1.2.8/minigzipsh
|
||||
third_party/zlib-1.2.8/zlib.pc
|
||||
|
||||
cmake
|
||||
!cmake/FindMINIZIP.cmake
|
||||
!cmake/FindPackage.cmake
|
||||
!cmake/i686-toolchain.cmake
|
||||
|
||||
.vscode
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Indent rules for libxlsxwriter.
|
||||
*
|
||||
* The rules for user defined typedefs can be update as follows:
|
||||
*
|
||||
perl -i -pe 'print and last if /[l]ibxlsxwriter typedefs/' .indent.pro
|
||||
ack -h typedef include/xlsxwriter/*.h src/*.c | perl -lne 'print "-T $1" if /\w+\s+\w+\s+(\w+)/' | sort >> .indent.pro
|
||||
*
|
||||
*/
|
||||
|
||||
/* Command line options used with GNU indent 2.2.10 */
|
||||
--braces-on-if-line
|
||||
--braces-on-struct-decl-line
|
||||
--case-indentation 4
|
||||
--continue-at-parentheses
|
||||
--declaration-comment-column 0
|
||||
--format-first-column-comments
|
||||
--honour-newlines
|
||||
--ignore-profile
|
||||
--indent-label 0
|
||||
--indent-level 4
|
||||
--no-space-after-function-call-names
|
||||
--no-tabs
|
||||
--swallow-optional-blank-lines
|
||||
|
||||
/* Typedefs used in the code. */
|
||||
-T int8_t
|
||||
-T int16_t
|
||||
-T int32_t
|
||||
-T int64_t
|
||||
-T uint8_t
|
||||
-T uint16_t
|
||||
-T uint32_t
|
||||
-T uint64_t
|
||||
-T ssize_t
|
||||
-T size_t
|
||||
-T time_t
|
||||
|
||||
-T LIST_ENTRY
|
||||
-T RB_ENTRY
|
||||
-T SLIST_ENTRY
|
||||
-T STAILQ_ENTRY
|
||||
-T TAILQ_ENTRY
|
||||
|
||||
/* libxlsxwriter typedefs. */
|
||||
-T lxw_app
|
||||
-T lxw_author_id
|
||||
-T lxw_autofilter
|
||||
-T lxw_border
|
||||
-T lxw_button_options
|
||||
-T lxw_cell
|
||||
-T lxw_chart
|
||||
-T lxw_chart_axis
|
||||
-T lxw_chart_axis_display_unit
|
||||
-T lxw_chart_axis_label_alignment
|
||||
-T lxw_chart_axis_label_position
|
||||
-T lxw_chart_axis_tick_mark
|
||||
-T lxw_chart_axis_tick_position
|
||||
-T lxw_chart_axis_type
|
||||
-T lxw_chart_blank
|
||||
-T lxw_chart_custom_label
|
||||
-T lxw_chart_data_label
|
||||
-T lxw_chart_error_bar_axis
|
||||
-T lxw_chart_error_bar_cap
|
||||
-T lxw_chart_error_bar_direction
|
||||
-T lxw_chart_error_bar_type
|
||||
-T lxw_chart_fill
|
||||
-T lxw_chart_font
|
||||
-T lxw_chart_gridline
|
||||
-T lxw_chart_label_position
|
||||
-T lxw_chart_label_separator
|
||||
-T lxw_chart_legend
|
||||
-T lxw_chart_legend_position
|
||||
-T lxw_chart_line
|
||||
-T lxw_chart_line_dash_type
|
||||
-T lxw_chart_marker
|
||||
-T lxw_chart_marker_type
|
||||
-T lxw_chart_options
|
||||
-T lxw_chart_pattern
|
||||
-T lxw_chart_pattern_type
|
||||
-T lxw_chart_point
|
||||
-T lxw_chart_series
|
||||
-T lxw_chart_title
|
||||
-T lxw_chart_trendline_type
|
||||
-T lxw_chart_type
|
||||
-T lxw_chartsheet
|
||||
-T lxw_chartsheet_name
|
||||
-T lxw_col_options
|
||||
-T lxw_col_t
|
||||
-T lxw_color_t
|
||||
-T lxw_comment
|
||||
-T lxw_comment_options
|
||||
-T lxw_cond_format_hash_element
|
||||
-T lxw_cond_format_obj
|
||||
-T lxw_conditional_format
|
||||
-T lxw_content_types
|
||||
-T lxw_core
|
||||
-T lxw_custom
|
||||
-T lxw_custom_property
|
||||
-T lxw_data_val_obj
|
||||
-T lxw_data_validation
|
||||
-T lxw_datetime
|
||||
-T lxw_defined_name
|
||||
-T lxw_doc_properties
|
||||
-T lxw_drawing
|
||||
-T lxw_drawing_coords
|
||||
-T lxw_drawing_object
|
||||
-T lxw_drawing_rel_id
|
||||
-T lxw_error
|
||||
-T lxw_fill
|
||||
-T lxw_filter_rule
|
||||
-T lxw_filter_rule_obj
|
||||
-T lxw_font
|
||||
-T lxw_format
|
||||
-T lxw_hash_element
|
||||
-T lxw_hash_table
|
||||
-T lxw_header_footer_options
|
||||
-T lxw_heading_pair
|
||||
-T lxw_image_md5
|
||||
-T lxw_image_options
|
||||
-T lxw_merged_range
|
||||
-T lxw_metadata
|
||||
-T lxw_object_properties
|
||||
-T lxw_packager
|
||||
-T lxw_panes
|
||||
-T lxw_part_name
|
||||
-T lxw_print_area
|
||||
-T lxw_protection
|
||||
-T lxw_protection_obj
|
||||
-T lxw_rel_tuple
|
||||
-T lxw_relationships
|
||||
-T lxw_repeat_cols
|
||||
-T lxw_repeat_rows
|
||||
-T lxw_rich_string_tuple
|
||||
-T lxw_row
|
||||
-T lxw_row_col_options
|
||||
-T lxw_row_t
|
||||
-T lxw_selection
|
||||
-T lxw_series_data_point
|
||||
-T lxw_series_error_bars
|
||||
-T lxw_series_range
|
||||
-T lxw_sheet
|
||||
-T lxw_sst
|
||||
-T lxw_styles
|
||||
-T lxw_table
|
||||
-T lxw_table_column
|
||||
-T lxw_table_obj
|
||||
-T lxw_table_options
|
||||
-T lxw_theme
|
||||
-T lxw_tuple
|
||||
-T lxw_vml
|
||||
-T lxw_vml_obj
|
||||
-T lxw_workbook
|
||||
-T lxw_workbook_options
|
||||
-T lxw_worksheet
|
||||
-T lxw_worksheet_init_data
|
||||
-T lxw_worksheet_name
|
||||
@@ -0,0 +1,424 @@
|
||||
# :copyright: (c) 2017 Alex Huszagh.
|
||||
# :license: FreeBSD, see LICENSE.txt for more details.
|
||||
|
||||
# Description
|
||||
# ===========
|
||||
#
|
||||
# Use:
|
||||
# Move to a custom directory, ideally out of source, and
|
||||
# type `cmake $LXW_SOURCE $FLAGS`, where `LXW_SOURCE` is the
|
||||
# path to the libxlsxwriter project, and `FLAGS` are custom
|
||||
# flags to pass to the compiler.
|
||||
#
|
||||
# Example:
|
||||
# For example, in the project directory, to build libxlsxwriter
|
||||
# and the unittests in release mode, type:
|
||||
# mkdir build && cd build
|
||||
# cmake .. -DBUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release
|
||||
# cmake --build . --config Release
|
||||
# ctest -C Release -V
|
||||
# cmake --build . --config Release --target install
|
||||
#
|
||||
# If using a Makefile generator, you may use the simpler
|
||||
# mkdir build && cd build
|
||||
# cmake .. -DBUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release
|
||||
# make
|
||||
# make test
|
||||
# make install
|
||||
#
|
||||
# Flags:
|
||||
# ZLIB_ROOT
|
||||
# The ZLIB root directory can be specified either through
|
||||
# an environment variable (`export ZLIB_ROOT=/usr/include`)
|
||||
# or using a flag with CMake (`-DZLIB_ROOT:STRING=/usr/include`).
|
||||
# This sets the preferred search path for the ZLIB installation.
|
||||
#
|
||||
# BUILD_TESTS
|
||||
# Build unittests (default off). To build the unittests,
|
||||
# pass `-DBUILD_TESTS=ON` during configuration.
|
||||
#
|
||||
# BUILD_EXAMPLES
|
||||
# Build example files (default off). To build the examples,
|
||||
# pass `-DBUILD_EXAMPLES=ON` during configuration.
|
||||
#
|
||||
# USE_STANDARD_TMPFILE
|
||||
# Use the standard tmpfile() function (default off). To enable
|
||||
# the standard tmpfile, pass `-DUSE_STANDARD_TMPFILE=ON`
|
||||
# during configuration. This may produce bugs while cross-
|
||||
# compiling or using MinGW/MSYS.
|
||||
#
|
||||
# USE_DTOA_LIBRARY
|
||||
# Use the third party emyg_dtoa() library (default off). The
|
||||
# emyg_dtoa() library is used to avoid sprintf double issues with
|
||||
# different locale settings. To enable this library, pass
|
||||
# `-DUSE_DTOA_LIBRARY=ON` during configuration.
|
||||
#
|
||||
# USE_NO_MD5
|
||||
# Compile without third party MD5 support. This will turn off the
|
||||
# functionality of avoiding duplicate image files in the output xlsx
|
||||
# file. To enable this option pass `-DUSE_NO_MD5=ON` during
|
||||
# configuration.
|
||||
#
|
||||
# USE_OPENSSL_MD5 Compile with OpenSSL MD5 support. This will link
|
||||
# against libcrypto for MD5 support rather than using the local MD5
|
||||
# support. MD5 support is required to avoid duplicate image files in
|
||||
# the output xlsx file. To enable this option pass
|
||||
# `-DUSE_OPENSSL_MD5=ON` during configuration.
|
||||
#
|
||||
# USE_STATIC_MSVC_RUNTIME
|
||||
# Use the static msvc runtime library when compiling with msvc (default off)
|
||||
# To enable, pass `-DUSE_STATIC_MSVC_RUNTIME` during configuration.
|
||||
#
|
||||
# Toolchains:
|
||||
# On multiarch Linux systems, which can build and run multiple
|
||||
# binary targets on the same system, we include an `i686-toolchain`
|
||||
# file to enable building i686 (x86 32-bit) targets on x86_64 systems.
|
||||
# To use the i686 toolchain, pass the `-DCMAKE_TOOLCHAIN_FILE` option
|
||||
# during CMake configuration. For example, from the build directory,
|
||||
# you would use:
|
||||
# cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/i686-toolchain.cmake
|
||||
#
|
||||
# CMake Options:
|
||||
# CMake sets debug and release builds with the `CMAKE_BUILD_TYPE`
|
||||
# option, which can be set as a flag during configuration.
|
||||
# To build in release mode, pass `-DCMAKE_BUILD_TYPE=Release`
|
||||
# during configuration.
|
||||
#
|
||||
# CMake sets the creation of static and shared libraries with the
|
||||
# `BUILD_SHARED_LIBS` option, which can be set as a flag during
|
||||
# configuration. To build a static library, pass
|
||||
# `-DBUILD_SHARED_LIBS=OFF` during configuration.
|
||||
#
|
||||
# Generators:
|
||||
# CMake also supports custom build generators, such as MakeFiles,
|
||||
# Ninja, Visual Studio, and XCode. For example, to generate
|
||||
# a Visual Studio solution, configure with:
|
||||
# cmake .. -G "Visual Studio 14 2015 Win64"
|
||||
#
|
||||
# For more information on using generators, see:
|
||||
# https://cmake.org/cmake/help/v3.0/manual/cmake-generators.7.html
|
||||
#
|
||||
|
||||
set(CMAKE_LEGACY_CYGWIN_WIN32 1)
|
||||
if(MSVC)
|
||||
cmake_minimum_required(VERSION 3.4)
|
||||
else()
|
||||
cmake_minimum_required(VERSION 3.1)
|
||||
endif()
|
||||
|
||||
SET(XLSX_PROJECT_NAME "xlsxwriter" CACHE STRING "Optional project and binary name")
|
||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
project(${XLSX_PROJECT_NAME} C)
|
||||
enable_testing()
|
||||
|
||||
# POLICY
|
||||
# ------
|
||||
|
||||
# The use of the word ZLIB_ROOT should still work prior to "3.12.0",
|
||||
# just it's been generalized for all packages now. Just set the policy
|
||||
# to new, so we use it, and it will be used prior to 3.12 anyway.
|
||||
if(${CMAKE_VERSION} VERSION_GREATER "3.12" OR ${CMAKE_VERSION} VERSION_EQUAL "3.12")
|
||||
cmake_policy(SET CMP0074 NEW)
|
||||
endif()
|
||||
|
||||
# OPTIONS
|
||||
# -------
|
||||
SET(ZLIB_ROOT "" CACHE STRING "Optional root for the ZLIB installation")
|
||||
|
||||
option(BUILD_TESTS "Build libxlsxwriter tests" OFF)
|
||||
option(BUILD_EXAMPLES "Build libxlsxwriter examples" OFF)
|
||||
option(USE_SYSTEM_MINIZIP "Use system minizip installation" OFF)
|
||||
option(USE_STANDARD_TMPFILE "Use the C standard library's tmpfile()" OFF)
|
||||
option(USE_NO_MD5 "Build libxlsxwriter without third party MD5 lib" OFF)
|
||||
option(USE_OPENSSL_MD5 "Build libxlsxwriter with the OpenSSL MD5 lib" OFF)
|
||||
option(USE_MEM_FILE "Use fmemopen()/open_memstream() in place of temporary files" OFF)
|
||||
option(IOAPI_NO_64 "Disable 64-bit filesystem support" OFF)
|
||||
option(USE_DTOA_LIBRARY "Use the locale independent third party Milo Yip DTOA library" OFF)
|
||||
|
||||
if(MSVC)
|
||||
option(USE_STATIC_MSVC_RUNTIME "Use the static runtime library" OFF)
|
||||
endif()
|
||||
|
||||
if(DEFINED ENV{${ZLIB_ROOT}})
|
||||
set(ZLIB_ROOT $ENV{ZLIB_ROOT})
|
||||
endif()
|
||||
|
||||
if(IOAPI_NO_64)
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS IOAPI_NO_64=1)
|
||||
endif()
|
||||
|
||||
# CONFIGURATIONS
|
||||
# --------------
|
||||
if(USE_SYSTEM_MINIZIP)
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_SYSTEM_MINIZIP)
|
||||
endif()
|
||||
|
||||
if(USE_STANDARD_TMPFILE)
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_STANDARD_TMPFILE)
|
||||
endif()
|
||||
|
||||
if(NOT USE_OPENSSL_MD5 AND USE_NO_MD5)
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_NO_MD5)
|
||||
endif()
|
||||
|
||||
if(USE_OPENSSL_MD5)
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_OPENSSL_MD5)
|
||||
if(NOT MSVC)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-deprecated-declarations")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(USE_MEM_FILE OR USE_FMEMOPEN)
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_FMEMOPEN)
|
||||
endif()
|
||||
|
||||
if(USE_DTOA_LIBRARY)
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_DTOA_LIBRARY)
|
||||
endif()
|
||||
|
||||
if(NOT BUILD_SHARED_LIBS)
|
||||
if(UNIX)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
elseif(MINGW OR MSYS)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static -static-libgcc -Wno-char-subscripts -Wno-long-long")
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_FILE32API)
|
||||
elseif(MSVC)
|
||||
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /Fd\"${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pdb\"")
|
||||
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /Ox /Zi /Fd\"${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pdb\"")
|
||||
set(CMAKE_C_FLAGS_MINSIZEREL "${CMAKE_C_FLAGS_MINSIZEREL} /Zi /Fd\"${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pdb\"")
|
||||
set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} /Fd\"${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pdb\"")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MSVC AND USE_STATIC_MSVC_RUNTIME)
|
||||
foreach(flag_var CMAKE_C_FLAGS
|
||||
CMAKE_C_FLAGS_DEBUG
|
||||
CMAKE_C_FLAGS_RELEASE
|
||||
CMAKE_C_FLAGS_MINSIZEREL
|
||||
CMAKE_C_FLAGS_RELWITHDEBINFO)
|
||||
if(${flag_var} MATCHES "/MD")
|
||||
string(REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# Configure pkg-config
|
||||
file(READ "include/xlsxwriter.h" ver)
|
||||
|
||||
string(REGEX MATCH "LXW_VERSION \"([^\"]+)\"" _ ${ver})
|
||||
set(VERSION ${CMAKE_MATCH_1})
|
||||
string(REGEX MATCH "LXW_SOVERSION \"([^\"]+)\"" _ ${ver})
|
||||
set(SOVERSION ${CMAKE_MATCH_1})
|
||||
set(PREFIX ${CMAKE_INSTALL_PREFIX})
|
||||
|
||||
configure_file(dev/release/pkg-config.txt xlsxwriter.pc @ONLY)
|
||||
|
||||
# INCLUDES
|
||||
# --------
|
||||
enable_language(CXX)
|
||||
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
|
||||
|
||||
# ZLIB
|
||||
find_package(ZLIB REQUIRED "1.0")
|
||||
list(APPEND LXW_PRIVATE_INCLUDE_DIRS ${ZLIB_INCLUDE_DIRS})
|
||||
message("zlib version: " ${ZLIB_VERSION})
|
||||
|
||||
# MINIZIP
|
||||
if (USE_SYSTEM_MINIZIP)
|
||||
find_package(MINIZIP REQUIRED "1.0")
|
||||
list(APPEND LXW_PRIVATE_INCLUDE_DIRS ${MINIZIP_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
# LIBRARY
|
||||
# -------
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS NOCRYPT NOUNCRYPT)
|
||||
|
||||
# Ensure CRT Secure warnings are disabled
|
||||
if(MSVC)
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS _CRT_SECURE_NO_WARNINGS)
|
||||
endif()
|
||||
|
||||
# Ensure "TESTING" macro is defined if building tests
|
||||
if(BUILD_TESTS)
|
||||
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS TESTING)
|
||||
endif()
|
||||
|
||||
file(GLOB LXW_SOURCES src/*.c)
|
||||
file(GLOB_RECURSE LXW_HEADERS RELATIVE include *.h)
|
||||
|
||||
if(NOT USE_SYSTEM_MINIZIP)
|
||||
list(APPEND LXW_SOURCES third_party/minizip/ioapi.c third_party/minizip/zip.c)
|
||||
if(MSVC)
|
||||
list(APPEND LXW_SOURCES third_party/minizip/iowin32.c)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT USE_STANDARD_TMPFILE)
|
||||
list(APPEND LXW_SOURCES third_party/tmpfileplus/tmpfileplus.c)
|
||||
endif()
|
||||
|
||||
if(NOT USE_OPENSSL_MD5 AND NOT USE_NO_MD5)
|
||||
list(APPEND LXW_SOURCES third_party/md5/md5.c)
|
||||
endif()
|
||||
|
||||
if(USE_OPENSSL_MD5)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
if(OpenSSL_FOUND)
|
||||
include_directories(${OPENSSL_INCLUDE_DIR})
|
||||
message(STATUS "OpenSSL version: ${OPENSSL_VERSION}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (USE_DTOA_LIBRARY)
|
||||
list(APPEND LXW_SOURCES third_party/dtoa/emyg_dtoa.c)
|
||||
endif()
|
||||
|
||||
set(LXW_PROJECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(LXW_LIB_DIR "${LXW_PROJECT_DIR}/lib")
|
||||
add_library(${PROJECT_NAME} "")
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES SOVERSION ${SOVERSION})
|
||||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE ${LXW_SOURCES}
|
||||
PUBLIC ${LXW_HEADERS}
|
||||
)
|
||||
target_link_libraries(${PROJECT_NAME} LINK_PUBLIC ${ZLIB_LIBRARIES} ${MINIZIP_LIBRARIES} ${LIB_CRYPTO} ${OPENSSL_CRYPTO_LIBRARY})
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE ${LXW_PRIVATE_COMPILE_DEFINITIONS})
|
||||
|
||||
# /utf-8 needs VS2015 Update 2 or above.
|
||||
# In CMake 3.7 and above, we can use (MSVC_VERSION GREATER_EQUAL 1900) here.
|
||||
if(MSVC AND NOT (MSVC_VERSION LESS 1900))
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8)
|
||||
endif()
|
||||
|
||||
if (WINDOWSSTORE)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE -DIOWIN32_USING_WINRT_API)
|
||||
endif()
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE ${LXW_PRIVATE_INCLUDE_DIRS}
|
||||
PUBLIC include include/xlsxwriter
|
||||
)
|
||||
|
||||
# TESTS
|
||||
# -----
|
||||
|
||||
# Create test and runner.
|
||||
#
|
||||
# Args:
|
||||
# sources Name of variable holding source files
|
||||
# target Test name
|
||||
#
|
||||
|
||||
macro(CreateTest sources target)
|
||||
set(output_name xlsxwriter_${target})
|
||||
set(dependencies ${output_name})
|
||||
|
||||
add_executable(${output_name} ${${sources}})
|
||||
target_link_libraries(${output_name} ${PROJECT_NAME})
|
||||
target_compile_definitions(${output_name} PRIVATE TESTING COLOR_OK)
|
||||
add_test(NAME ${output_name}
|
||||
COMMAND ${output_name}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
endmacro(CreateTest)
|
||||
|
||||
file(GLOB LXW_UTILITY_SOURCES test/unit/utility/test*.c)
|
||||
file(GLOB LXW_XMLWRITER_SOURCES test/unit/xmlwriter/test*.c)
|
||||
file(GLOB LXW_WORKSHEET_SOURCES test/unit/worksheet/test*.c)
|
||||
file(GLOB LXW_SST_SOURCES test/unit/sst/test*.c)
|
||||
file(GLOB LXW_WORKBOOK_SOURCES test/unit/workbook/test*.c)
|
||||
file(GLOB LXW_APP_SOURCES test/unit/app/test*.c)
|
||||
file(GLOB LXW_CONTENTTYPES_SOURCES test/unit/content_types/test*.c)
|
||||
file(GLOB LXW_CORE_SOURCES test/unit/core/test*.c)
|
||||
file(GLOB LXW_RELATIONSHIPS_SOURCES test/unit/relationships/test*.c)
|
||||
file(GLOB LXW_FORMAT_SOURCES test/unit/format/test*.c)
|
||||
file(GLOB LXW_STYLES_SOURCES test/unit/styles/test*.c)
|
||||
file(GLOB LXW_DRAWING_SOURCES test/unit/drawing/test*.c)
|
||||
file(GLOB LXW_CHART_SOURCES test/unit/chart/test*.c)
|
||||
file(GLOB LXW_CUSTOM_SOURCES test/unit/custom/test*.c)
|
||||
file(GLOB LXW_FUNCTIONAL_SOURCES test/functional/src/*.c)
|
||||
|
||||
set(LXW_UNIT_SOURCES
|
||||
test/unit/test_all.c
|
||||
${LXW_UTILITY_SOURCES}
|
||||
${LXW_XMLWRITER_SOURCES}
|
||||
${LXW_WORKSHEET_SOURCES}
|
||||
${LXW_SST_SOURCES}
|
||||
${LXW_WORKBOOK_SOURCES}
|
||||
${LXW_APP_SOURCES}
|
||||
${LXW_CONTENTTYPES_SOURCES}
|
||||
${LXW_CORE_SOURCES}
|
||||
${LXW_RELATIONSHIPS_SOURCES}
|
||||
${LXW_FORMAT_SOURCES}
|
||||
${LXW_STYLES_SOURCES}
|
||||
${LXW_DRAWING_SOURCES}
|
||||
${LXW_CHART_SOURCES}
|
||||
${LXW_CUSTOM_SOURCES}
|
||||
)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
# unit tests
|
||||
CreateTest(LXW_UNIT_SOURCES unit)
|
||||
|
||||
# functional tests
|
||||
find_package(Python COMPONENTS Interpreter REQUIRED)
|
||||
find_program(Pytest_EXECUTABLE NAMES pytest)
|
||||
|
||||
if (NOT Pytest_EXECUTABLE)
|
||||
message("Please install the Python pytest library to run functional tests:")
|
||||
message(" pip install pytest\n")
|
||||
endif()
|
||||
|
||||
foreach(source ${LXW_FUNCTIONAL_SOURCES})
|
||||
get_filename_component(basename ${source} NAME_WE)
|
||||
add_executable(${basename} ${source})
|
||||
target_link_libraries(${basename} xlsxwriter)
|
||||
set_target_properties(${basename} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "test/functional/src")
|
||||
endforeach(source)
|
||||
|
||||
add_custom_command(TARGET xlsxwriter_unit POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/test/functional test/functional
|
||||
)
|
||||
|
||||
if(USE_NO_MD5)
|
||||
add_test(NAME functional
|
||||
COMMAND pytest -v test/functional -m "not skipif"
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
else()
|
||||
add_test(NAME functional
|
||||
COMMAND pytest -v test/functional
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
||||
# EXAMPLES
|
||||
# --------
|
||||
file(GLOB LXW_EXAMPLE_SOURCES examples/*.c)
|
||||
|
||||
if(BUILD_EXAMPLES)
|
||||
foreach(source ${LXW_EXAMPLE_SOURCES})
|
||||
get_filename_component(basename ${source} NAME_WE)
|
||||
add_executable(${basename} ${source})
|
||||
target_link_libraries(${basename} ${PROJECT_NAME})
|
||||
set_target_properties(${basename} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "examples")
|
||||
endforeach(source)
|
||||
endif()
|
||||
|
||||
# INSTALL
|
||||
# -------
|
||||
include(GNUInstallDirs)
|
||||
|
||||
install(TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
install(FILES include/xlsxwriter.h DESTINATION include)
|
||||
install(DIRECTORY include/xlsxwriter
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||
FILES_MATCHING PATTERN "*.h"
|
||||
)
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/xlsxwriter.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
@@ -0,0 +1,226 @@
|
||||
# libxlsxwriter: Reporting Bugs and submitting Pull Requests
|
||||
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
Here are some tips on reporting bugs in `libxlsxwriter`.
|
||||
|
||||
### Upgrade to the latest version of the library
|
||||
|
||||
Upgrade to the latest version of the library since the bug you are reporting
|
||||
may already be fixed.
|
||||
|
||||
Check the [Changes][changes] section of the documentation to see what has
|
||||
changed in the latest versions.
|
||||
|
||||
[changes]: http://libxlsxwriter.github.io/changes.html
|
||||
|
||||
You can check which version of `libxlsxwriter` that you are using by checking
|
||||
the `xlsxwriter.h` header file or by adding the following to your program:
|
||||
|
||||
```C
|
||||
#include <stdio.h>
|
||||
#include "xlsxwriter.h"
|
||||
|
||||
int main() {
|
||||
|
||||
printf("Libxlsxwriter version = %s\n", lxw_version());
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Read the documentation
|
||||
|
||||
Read or search the `libxlsxwriter` [documentation][docs] to see if the issue
|
||||
you are encountering is already explained.
|
||||
|
||||
[docs]: http://libxlsxwriter.github.io/index.html
|
||||
|
||||
### Look at the example programs
|
||||
|
||||
There are many [examples programs][examples] in the distribution. Try to
|
||||
identify an example program that corresponds to your query and adapt it to use
|
||||
as a bug report.
|
||||
|
||||
[examples]: http://libxlsxwriter.github.io/examples.html
|
||||
|
||||
|
||||
### Use the xlsxwriter Issue Tracker
|
||||
|
||||
The [libxlsxwriter issue tracker][issues] is on GitHub.
|
||||
|
||||
[issues]: https://github.com/jmcnamara/libxlsxwriter/issues
|
||||
|
||||
|
||||
### Tips for submitting a bug report
|
||||
|
||||
1. Describe the problem as clearly and as concisely as possible.
|
||||
2. Include a sample program. This is probably the most important step.
|
||||
It is generally easier to describe a problem in code than in written
|
||||
prose.
|
||||
3. The sample program should be as small as possible to demonstrate the
|
||||
problem. Don't copy and paste large non-relevant sections of your
|
||||
program.
|
||||
|
||||
A sample bug report is shown below. This format helps analyze and respond to
|
||||
the bug report more quickly.
|
||||
|
||||
|
||||
> Subject: Issue with SOMETHING
|
||||
>
|
||||
> Greetings,
|
||||
>
|
||||
> I am using libxlsxwriter to do SOMETHING but it appears to do SOMETHING ELSE.
|
||||
>
|
||||
> I am using CC version X.Y.Z, OS = uname and libxlsxwriter x.y.z.
|
||||
>
|
||||
> Here is some code that demonstrates the problem:
|
||||
>
|
||||
>
|
||||
>```C
|
||||
>#include "xlsxwriter.h"
|
||||
>
|
||||
>int main() {
|
||||
>
|
||||
> lxw_workbook *workbook = workbook_new("bug_report.xlsx");
|
||||
> lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
|
||||
>
|
||||
> worksheet_write_string(worksheet, 0, 0, "Hello", NULL);
|
||||
> worksheet_write_number(worksheet, 1, 0, 123, NULL);
|
||||
>
|
||||
> return workbook_close(workbook);
|
||||
>}
|
||||
>```
|
||||
>
|
||||
|
||||
|
||||
# Pull Requests and Contributing to Libxlsxwriter
|
||||
|
||||
All patches and pull requests are welcome but in general you should start with
|
||||
an issue tracker to describe what you intend to do before you do it.
|
||||
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Pull requests and new feature proposals must start with an [issue
|
||||
tracker][issues]. This serves as the focal point for the design discussion.
|
||||
2. Describe what you plan to do. If there are API changes add some code
|
||||
example to demonstrate them.
|
||||
3. Fork the repository.
|
||||
4. Run all the tests to make sure the current code works on your system using
|
||||
`make test`. See the [Running the Test Suite][tests] section of the docs
|
||||
for instructions.
|
||||
5. Create a feature branch for your new feature.
|
||||
|
||||
|
||||
[tests]: http://libxlsxwriter.github.io/running_the_tests.html
|
||||
|
||||
### Code Style
|
||||
|
||||
The code style is mainly K&R style with 4 space indents.
|
||||
|
||||
The author uses GNU indent (`gindent`) 2.2.10 with the following options:
|
||||
|
||||
```
|
||||
--braces-on-if-line
|
||||
--braces-on-struct-decl-line
|
||||
--case-indentation 4
|
||||
--continue-at-parentheses
|
||||
--declaration-comment-column 0
|
||||
--format-first-column-comments
|
||||
--honour-newlines
|
||||
--ignore-profile
|
||||
--indent-label 0
|
||||
--indent-level 4
|
||||
--no-space-after-function-call-names
|
||||
--no-tabs
|
||||
--swallow-optional-blank-lines
|
||||
```
|
||||
|
||||
The [indent configuration file][indentpro] is available in the repo. The code
|
||||
can be indented automatically if the same version of `gindent` is used with
|
||||
the following make command:
|
||||
|
||||
```shell
|
||||
make indent
|
||||
```
|
||||
|
||||
Note, make sure you have backed up your files or added them to the index
|
||||
before running this command.
|
||||
|
||||
In general follow the existing style in the code.
|
||||
|
||||
[indentpro]: https://github.com/jmcnamara/libxlsxwriter/blob/master/.indent.pro
|
||||
|
||||
### Writing and Running Tests
|
||||
|
||||
Any significant features should be accompanied by a test. See the `test`
|
||||
directory and the [Running the Test Suite][tests] section of the docs for
|
||||
details of the test setup.
|
||||
|
||||
The tests can be run as follows:
|
||||
|
||||
```shell
|
||||
make test
|
||||
```
|
||||
Same as:
|
||||
|
||||
```shell
|
||||
make test_unit
|
||||
make test_functional
|
||||
```
|
||||
|
||||
The functional tests require the Python module [pytest][pytest] as a test runner.
|
||||
|
||||
If you have `valgrind` installed you can use the test suite to check for memory leaks:
|
||||
|
||||
```shell
|
||||
make test_valgrind
|
||||
```
|
||||
|
||||
When you push your changes they will also be tested automatically using
|
||||
[GitHub Actions][actions].
|
||||
|
||||
[actions]: https://github.com/jmcnamara/libxlsxwriter/actions
|
||||
[pytest]: http://pytest.org/
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
The `libxlsxwriter` documentation is written in Doxygen format in the header
|
||||
files and in additional `.dox` files in the `docs/src` directory of the
|
||||
repo. The documentation can be built as follows:
|
||||
|
||||
```shell
|
||||
make docs
|
||||
open docs/html/index.html
|
||||
```
|
||||
|
||||
|
||||
### Example programs
|
||||
|
||||
If applicable add an example program to the `examples` directory. Example
|
||||
files can be built using:
|
||||
|
||||
```shell
|
||||
make docs
|
||||
```
|
||||
|
||||
### Copyright and License
|
||||
|
||||
Copyright remains with the original author. Do not include additional
|
||||
copyright claims or Licensing requirements. GitHub and the `git` repository
|
||||
will record your contribution and it will be acknowledged it in the Changes
|
||||
file.
|
||||
|
||||
|
||||
### Submitting the Pull Request
|
||||
|
||||
If your change involves several incremental `git` commits then `rebase` or
|
||||
`squash` them onto another branch so that the Pull Request is a single commit
|
||||
or a small number of logical commits.
|
||||
|
||||
Push your changes to GitHub and submit the Pull Request with a hash link to
|
||||
the to the Issue tracker that was opened above.
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
|
||||
@page license License
|
||||
|
||||
Libxlsxwriter is released under a FreeBSD license:
|
||||
|
||||
Copyright 2014-2022, John McNamara <jmcnamara@cpan.org>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The views and conclusions contained in the software and documentation are
|
||||
those of the authors and should not be interpreted as representing
|
||||
official policies, either expressed or implied, of the FreeBSD Project.
|
||||
|
||||
|
||||
Libxlsxwriter includes the `queue.h` and `tree.h` macros from FreeBSD. It also
|
||||
includes and, unless overridden, uses the optional libraries `minizip`,
|
||||
`tmpfileplus` and `md5`. It also includes the `emyg_dtoa` library but doesn't
|
||||
use it by default. These components have the following licenses:
|
||||
|
||||
|
||||
Queue.h from FreeBSD:
|
||||
|
||||
Copyright (c) 1991, 1993
|
||||
The Regents of the University of California. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
4. Neither the name of the University nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
|
||||
|
||||
Tree.h from FreeBSD:
|
||||
|
||||
Copyright 2002 Niels Provos <provos@citi.umich.edu>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
The `minizip` files used in the libxlsxwriter source tree are taken from the
|
||||
`zlib` ` contrib/minizip` directory. [Zlib](http://www.zlib.net) has the
|
||||
following License/Copyright:
|
||||
|
||||
(C) 1995-2013 Jean-loup Gailly and Mark Adler
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
Jean-loup Gailly Mark Adler
|
||||
jloup@gzip.org madler@alumni.caltech.edu
|
||||
|
||||
The `minizip` files have the following additional copyright declarations:
|
||||
|
||||
Copyright (C) 1998-2010 Gilles Vollant
|
||||
(minizip) ( http://www.winimage.com/zLibDll/minizip.html )
|
||||
|
||||
Modifications for Zip64 support
|
||||
Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
|
||||
|
||||
Note, it is possible to compile libxlsxwriter without statically linking the
|
||||
`minizip` files and instead dynamically linking to `lminizip`, see
|
||||
@ref gsg_minizip.
|
||||
|
||||
[Tmpfileplus](http://www.di-mgt.com.au/c_function_to_create_temp_file.html)
|
||||
has the following license:
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
Copyright (c) 2012-16 David Ireland, DI Management Services Pty Ltd
|
||||
<http://www.di-mgt.com.au/contact/>.
|
||||
|
||||
See the [Mozilla Public License, v. 2.0](http://mozilla.org/MPL/2.0/).
|
||||
|
||||
Note, it is possible to compile libxlsxwriter using the standard library
|
||||
`tmpfile()` function instead of `tmpfileplus`, see @ref gsg_tmpdir.
|
||||
|
||||
The [Milo Yip DTOA library](https://github.com/miloyip/dtoa-benchmark) for
|
||||
converting doubles to strings. It has the following license:
|
||||
|
||||
Copyright (C) 2015 Doug Currie
|
||||
based on dtoa_milo.h
|
||||
Copyright (C) 2014 Milo Yip
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
This Milo Yip DTOA library (emyg_dtoa) is used to avoid issues where the
|
||||
standard sprintf() dtoa function changes output based on locale settings. It
|
||||
is also 40-50% faster than the standard dtoa for raw numeric data. The use of
|
||||
this library is optional. If you wish to use it you can pass
|
||||
`USE_DTOA_LIBRARY=1` to make when compiling.
|
||||
|
||||
[Openwall MD5](https://openwall.info/wiki/people/solar/software/public-domain-source-code/md5)
|
||||
has the following licence:
|
||||
|
||||
This software was written by Alexander Peslyak in 2001. No copyright is
|
||||
claimed, and the software is hereby placed in the public domain.
|
||||
In case this attempt to disclaim copyright and place the software in the
|
||||
public domain is deemed null and void, then the software is
|
||||
Copyright (c) 2001 Alexander Peslyak and it is hereby released to the
|
||||
general public under the following terms:
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted.
|
||||
|
||||
There's ABSOLUTELY NO WARRANTY, express or implied.
|
||||
|
||||
(This is a heavily cut-down "BSD license".)
|
||||
|
||||
Note, the MD5 library is used to avoid including duplicate image files in the
|
||||
xlsx file. If you don't want to use this code, and the additional licence, you
|
||||
can use OpenSSL's MD5 functions instead by passing `USE_OPENSSL_MD5=1` to
|
||||
make. If this functionality isn't required it is possible to compile
|
||||
libxlsxwriter without image deduplication by passing `USE_NO_MD5=1` to make.
|
||||
|
||||
See also @ref gsg_md5.
|
||||
|
||||
Next: @ref changes
|
||||
*/
|
||||
@@ -0,0 +1,275 @@
|
||||
###############################################################################
|
||||
#
|
||||
# Makefile for libxlsxwriter library.
|
||||
#
|
||||
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
|
||||
# Keep the output quiet by default.
|
||||
Q=@
|
||||
ifdef V
|
||||
Q=
|
||||
endif
|
||||
|
||||
DESTDIR ?=
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
PYTEST ?= py.test
|
||||
PYTESTFILES ?= test
|
||||
|
||||
VERSION = $(shell sed -n -e 's/.*LXW_VERSION \"\(.*\)\"/\1/p' include/xlsxwriter.h)
|
||||
SOVERSION = $(shell sed -n -e 's/.*LXW_SOVERSION \"\(.*\)\"/\1/p' include/xlsxwriter.h)
|
||||
|
||||
.PHONY: docs tags examples third_party
|
||||
|
||||
# Build libxlsxwriter.
|
||||
all : third_party
|
||||
$(Q)$(MAKE) -C src
|
||||
|
||||
# Build the third party libs.
|
||||
third_party :
|
||||
ifndef USE_SYSTEM_MINIZIP
|
||||
$(Q)$(MAKE) -C third_party/minizip
|
||||
endif
|
||||
ifndef USE_STANDARD_TMPFILE
|
||||
$(Q)$(MAKE) -C third_party/tmpfileplus
|
||||
endif
|
||||
ifndef USE_NO_MD5
|
||||
ifndef USE_OPENSSL_MD5
|
||||
$(Q)$(MAKE) -C third_party/md5
|
||||
endif
|
||||
endif
|
||||
ifdef USE_DTOA_LIBRARY
|
||||
$(Q)$(MAKE) -C third_party/dtoa
|
||||
endif
|
||||
|
||||
# Build a macOS universal binary.
|
||||
universal_binary :
|
||||
$(Q)$(MAKE) clean
|
||||
$(Q)TARGET_ARCH="-target x86_64-apple-macos10.12" $(MAKE) all
|
||||
$(Q)mv lib/libxlsxwriter.a libxlsxwriter_x86_64.a
|
||||
$(Q)mv lib/libxlsxwriter.$(SOVERSION).dylib libxlsxwriter_x86_64.dylib
|
||||
|
||||
$(Q)$(MAKE) clean
|
||||
$(Q)TARGET_ARCH="-target arm64-apple-macos11" $(MAKE) all
|
||||
$(Q)mv lib/libxlsxwriter.a lib/libxlsxwriter_arm64.a
|
||||
$(Q)mv lib/libxlsxwriter.$(SOVERSION).dylib lib/libxlsxwriter_arm64.dylib
|
||||
$(Q)mv libxlsxwriter_x86_64.a libxlsxwriter_x86_64.dylib lib
|
||||
|
||||
$(Q)lipo -create -output lib/libxlsxwriter.a lib/libxlsxwriter_x86_64.a lib/libxlsxwriter_arm64.a
|
||||
$(Q)lipo -create -output lib/libxlsxwriter.$(SOVERSION).dylib lib/libxlsxwriter_x86_64.dylib lib/libxlsxwriter_arm64.dylib
|
||||
$(Q)rm -f lib/libxlsxwriter_x86_64.* lib/libxlsxwriter_arm64.*
|
||||
|
||||
# Build the example programs.
|
||||
examples : all
|
||||
$(Q)$(MAKE) -C examples
|
||||
|
||||
# Clean src and test directories.
|
||||
clean :
|
||||
$(Q)$(MAKE) clean -C src
|
||||
$(Q)$(MAKE) clean -C test/unit
|
||||
$(Q)$(MAKE) clean -C test/functional/src
|
||||
$(Q)$(MAKE) clean -C test/cpp
|
||||
$(Q)$(MAKE) clean -C examples
|
||||
$(Q)rm -rf docs/html
|
||||
$(Q)rm -rf test/functional/__pycache__
|
||||
$(Q)rm -f test/functional/*.pyc
|
||||
$(Q)rm -f lib/*
|
||||
$(Q)$(MAKE) clean -C third_party/minizip
|
||||
$(Q)$(MAKE) clean -C third_party/tmpfileplus
|
||||
$(Q)$(MAKE) clean -C third_party/md5
|
||||
$(Q)$(MAKE) clean -C third_party/dtoa
|
||||
|
||||
# Clean src and lib dir only, as a precursor for static analysis.
|
||||
clean_src :
|
||||
$(Q)$(MAKE) clean -C src
|
||||
$(Q)rm -f lib/*
|
||||
|
||||
# Run the unit tests.
|
||||
test : all test_cpp test_unit test_functional
|
||||
|
||||
# Test for C++ const correctness on APIs.
|
||||
test_const : all
|
||||
$(Q)$(MAKE) clean -C test/functional/src
|
||||
$(Q)! $(MAKE) -C test/functional/src CFLAGS=-Wwrite-strings 2>&1 | grep -A 1 "note:"
|
||||
|
||||
|
||||
# Run the functional tests.
|
||||
test_functional : all
|
||||
$(Q)$(MAKE) -C test/functional/src
|
||||
$(Q)$(PYTEST) test/functional -v -k $(PYTESTFILES)
|
||||
|
||||
# Run all tests.
|
||||
test_unit : all
|
||||
$(Q)$(MAKE) -C src test_lib
|
||||
$(Q)$(MAKE) -C test/unit test
|
||||
|
||||
# Test C++ compilation.
|
||||
test_cpp : all
|
||||
$(Q)$(MAKE) -C test/cpp
|
||||
|
||||
# Test Cmake. This test should really be done with Cmake in the cmake dir but
|
||||
# this is a workaround for now.
|
||||
test_cmake :
|
||||
ifneq ($(findstring m32,$(CFLAGS)),m32)
|
||||
$(Q)$(MAKE) -C src clean
|
||||
$(Q)cd cmake; cmake .. -DBUILD_TESTS=ON -DBUILD_EXAMPLES=ON; make clean; make; cp libxlsxwriter.a ../src/
|
||||
$(Q)cmake/xlsxwriter_unit
|
||||
$(Q)$(MAKE) -C test/functional/src
|
||||
$(Q)$(PYTEST) test/functional -v -k $(PYTESTFILES)
|
||||
else
|
||||
@echo "Skipping Cmake tests on 32 bit target."
|
||||
endif
|
||||
|
||||
# Test the functional test exes with valgrind (in 64bit mode only).
|
||||
test_valgrind : all
|
||||
ifndef NO_VALGRIND
|
||||
$(Q)$(MAKE) -C test/functional/src test_valgrind
|
||||
$(Q)$(MAKE) -C examples test_valgrind
|
||||
endif
|
||||
|
||||
# Minimal target for quick compile without creating the libs.
|
||||
test_compile :
|
||||
$(Q)$(MAKE) -C src test_compile
|
||||
|
||||
# Indent the source files with the .indent.pro settings.
|
||||
indent:
|
||||
$(Q)gindent src/*.c include/*.h include/xlsxwriter/*.h
|
||||
|
||||
tags:
|
||||
$(Q)rm -f TAGS
|
||||
$(Q)etags src/*.c include/*.h include/xlsxwriter/*.h
|
||||
|
||||
# Build the doxygen docs.
|
||||
doc: docs
|
||||
docs:
|
||||
$(Q)$(MAKE) -C docs
|
||||
@echo "Docs built."
|
||||
|
||||
docs_doxygen_only:
|
||||
$(Q)$(MAKE) -C docs docs_doxygen_only
|
||||
|
||||
docs_external:
|
||||
$(Q)make -C ../libxlsxwriter.github.io release
|
||||
|
||||
# Simple install.
|
||||
install: all
|
||||
$(Q)mkdir -p $(DESTDIR)$(PREFIX)/include
|
||||
$(Q)cp -R include/* $(DESTDIR)$(PREFIX)/include
|
||||
$(Q)mkdir -p $(DESTDIR)$(PREFIX)/lib
|
||||
$(Q)cp -R lib/* $(DESTDIR)$(PREFIX)/lib
|
||||
$(Q)mkdir -p $(DESTDIR)$(PREFIX)/lib/pkgconfig
|
||||
$(Q)sed -e 's|@PREFIX@|$(PREFIX)|g' -e 's|@VERSION@|$(VERSION)|g' dev/release/pkg-config.txt > $(DESTDIR)$(PREFIX)/lib/pkgconfig/xlsxwriter.pc
|
||||
|
||||
# Simpler uninstall.
|
||||
uninstall:
|
||||
$(Q)rm -rf $(DESTDIR)$(PREFIX)/include/xlsxwriter*
|
||||
$(Q)rm $(DESTDIR)$(PREFIX)/lib/libxlsxwriter.*
|
||||
$(Q)rm $(DESTDIR)$(PREFIX)/lib/pkgconfig/xlsxwriter.pc
|
||||
|
||||
# Strip the lib files.
|
||||
strip:
|
||||
$(Q)strip lib/*
|
||||
|
||||
# Run a coverity static analysis.
|
||||
coverity: clean_src third_party
|
||||
$(Q)rm -rf cov-int
|
||||
$(Q)rm -f libxlsxwriter-coverity.tgz
|
||||
$(Q)../../cov-analysis-linux64-2019.03/bin/cov-build --dir cov-int make -C src libxlsxwriter.a
|
||||
$(Q)tar -czf libxlsxwriter-coverity.tgz cov-int
|
||||
$(Q)$(MAKE) -C src clean
|
||||
$(Q)rm -f lib/*
|
||||
|
||||
# Run gcov coverage analysis.
|
||||
gcov: third_party
|
||||
$(Q)$(MAKE) -C src clean
|
||||
$(Q)$(MAKE) -C src GCOV="--coverage" OPT_LEVEL="-O0"
|
||||
$(Q)$(MAKE) -C src test_lib GCOV="--coverage"
|
||||
$(Q)$(MAKE) -C test/unit test GCOV="--coverage"
|
||||
$(Q)$(MAKE) -C test/functional/src GCOV="--coverage"
|
||||
$(Q)$(PYTEST) test/functional -v -k $(PYTESTFILES)
|
||||
$(Q)mkdir -p build
|
||||
$(Q)gcovr -r src --html-details -o build/libxlsxwriter_gcov.html
|
||||
$(Q)gcovr -r . -f src --sonarqube build/coverage.xml
|
||||
|
||||
# Run sonarcloud analysis.
|
||||
sonarcloud: gcov
|
||||
ifndef SONAR_TOKEN
|
||||
@echo "Please define SONAR_TOKEN to run this analysis."
|
||||
@exit 1
|
||||
endif
|
||||
$(Q)$(MAKE) clean
|
||||
$(Q)../sonar-scanner-4.6.1.2450-macosx/bin/build-wrapper-macosx-x86 --out-dir build make all
|
||||
$(Q)../sonar-scanner-4.6.1.2450-macosx/bin/sonar-scanner \
|
||||
-Dsonar.organization=jmcnamara-github \
|
||||
-Dsonar.projectKey=jmcnamara_libxlsxwriter \
|
||||
-Dsonar.projectName=libxlsxwriter \
|
||||
-Dsonar.projectVersion=$(VERSION) \
|
||||
-Dsonar.sources=src \
|
||||
-Dsonar.sourceEncoding=UTF-8 \
|
||||
-Dsonar.cfamily.build-wrapper-output=build \
|
||||
-Dsonar.working.directory=build/scannerwork \
|
||||
-Dsonar.host.url=https://sonarcloud.io \
|
||||
-Dsonar.cfamily.threads=4 \
|
||||
-Dsonar.coverageReportPaths=build/coverage.xml \
|
||||
-Dsonar.cfamily.cache.enabled=false
|
||||
|
||||
sonarcloud_no_gcov:
|
||||
ifndef SONAR_TOKEN
|
||||
@echo "Please define SONAR_TOKEN to run this analysis."
|
||||
@exit 1
|
||||
endif
|
||||
$(Q)$(MAKE) clean
|
||||
$(Q)../sonar-scanner-4.6.1.2450-macosx/bin/build-wrapper-macosx-x86 --out-dir build make all
|
||||
$(Q)../sonar-scanner-4.6.1.2450-macosx/bin/sonar-scanner \
|
||||
-Dsonar.organization=jmcnamara-github \
|
||||
-Dsonar.projectKey=jmcnamara_libxlsxwriter \
|
||||
-Dsonar.projectName=libxlsxwriter \
|
||||
-Dsonar.projectVersion=$(VERSION) \
|
||||
-Dsonar.sources=src \
|
||||
-Dsonar.sourceEncoding=UTF-8 \
|
||||
-Dsonar.cfamily.build-wrapper-output=build \
|
||||
-Dsonar.working.directory=build/scannerwork \
|
||||
-Dsonar.host.url=https://sonarcloud.io \
|
||||
-Dsonar.cfamily.threads=4 \
|
||||
-Dsonar.cfamily.cache.enabled=false
|
||||
|
||||
|
||||
# Run a scan-build static analysis.
|
||||
scan_build: clean_src third_party
|
||||
$(Q)scan-build make -C src libxlsxwriter.a
|
||||
$(Q)$(MAKE) -C src clean
|
||||
$(Q)rm -f lib/*
|
||||
|
||||
spellcheck:
|
||||
$(Q)for f in docs/src/*.dox; do aspell --lang=en_US --check $$f; done
|
||||
$(Q)for f in include/xlsxwriter/*.h; do aspell --lang=en_US --check $$f; done
|
||||
$(Q)for f in src/*.c; do aspell --lang=en_US --check $$f; done
|
||||
$(Q)for f in examples/*.c; do aspell --lang=en_US --check $$f; done
|
||||
$(Q)aspell --lang=en_US --check Changes.txt
|
||||
$(Q)aspell --lang=en_US --check Readme.md
|
||||
$(Q)aspell --lang=en_US --check docs/src/examples.txt
|
||||
|
||||
releasecheck:
|
||||
$(Q)dev/release/release_check.sh
|
||||
|
||||
release: releasecheck
|
||||
@echo
|
||||
@echo "Pushing to git main ..."
|
||||
$(Q)git push origin main
|
||||
$(Q)git push --tags
|
||||
|
||||
@echo
|
||||
@echo "Pushing updated docs ..."
|
||||
$(Q)make -C ../libxlsxwriter.github.io release
|
||||
|
||||
@echo
|
||||
@echo "Pushing the cocoapod ..."
|
||||
$(Q)pod trunk push libxlsxwriter.podspec --use-libraries
|
||||
|
||||
@echo
|
||||
@echo "Finished. Opening files."
|
||||
$(Q)open https://libxlsxwriter.github.io/changes.html
|
||||
$(Q)open https://cocoadocs.org/docsets/libxlsxwriter
|
||||
$(Q)open https://github.com/jmcnamara/libxlsxwriter
|
||||
$(Q)open https://github.com/jmcnamara/libxlsxwriter/releases
|
||||
@@ -0,0 +1,82 @@
|
||||
# libxlsxwriter
|
||||
|
||||
|
||||
Libxlsxwriter: A C library for creating Excel XLSX files.
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
## The libxlsxwriter library
|
||||
|
||||
Libxlsxwriter is a C library that can be used to write text, numbers, formulas
|
||||
and hyperlinks to multiple worksheets in an Excel 2007+ XLSX file.
|
||||
|
||||
It supports features such as:
|
||||
|
||||
- 100% compatible Excel XLSX files.
|
||||
- Full Excel formatting.
|
||||
- Merged cells.
|
||||
- Defined names.
|
||||
- Autofilters.
|
||||
- Charts.
|
||||
- Data validation and drop down lists.
|
||||
- Conditional formatting.
|
||||
- Worksheet PNG/JPEG/GIF images.
|
||||
- Cell comments.
|
||||
- Support for adding Macros.
|
||||
- Memory optimization mode for writing large files.
|
||||
- Source code available on [GitHub](https://github.com/jmcnamara/libxlsxwriter).
|
||||
- FreeBSD license.
|
||||
- ANSI C.
|
||||
- Works with GCC, Clang, Xcode, MSVC 2015, ICC, TCC, MinGW, MingGW-w64/32.
|
||||
- Works on Linux, FreeBSD, OpenBSD, OS X, iOS and Windows. Also works on MSYS/MSYS2 and Cygwin.
|
||||
- Compiles for 32 and 64 bit.
|
||||
- Compiles and works on big and little endian systems.
|
||||
- The only dependency is on `zlib`.
|
||||
|
||||
Here is an example that was used to create the spreadsheet shown above:
|
||||
|
||||
|
||||
```C
|
||||
#include "xlsxwriter.h"
|
||||
|
||||
int main() {
|
||||
|
||||
/* Create a new workbook and add a worksheet. */
|
||||
lxw_workbook *workbook = workbook_new("demo.xlsx");
|
||||
lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
|
||||
|
||||
/* Add a format. */
|
||||
lxw_format *format = workbook_add_format(workbook);
|
||||
|
||||
/* Set the bold property for the format */
|
||||
format_set_bold(format);
|
||||
|
||||
/* Change the column width for clarity. */
|
||||
worksheet_set_column(worksheet, 0, 0, 20, NULL);
|
||||
|
||||
/* Write some simple text. */
|
||||
worksheet_write_string(worksheet, 0, 0, "Hello", NULL);
|
||||
|
||||
/* Text with formatting. */
|
||||
worksheet_write_string(worksheet, 1, 0, "World", format);
|
||||
|
||||
/* Write some numbers. */
|
||||
worksheet_write_number(worksheet, 2, 0, 123, NULL);
|
||||
worksheet_write_number(worksheet, 3, 0, 123.456, NULL);
|
||||
|
||||
/* Insert an image. */
|
||||
worksheet_insert_image(worksheet, 1, 2, "logo.png");
|
||||
|
||||
workbook_close(workbook);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
See the [full documentation](http://libxlsxwriter.github.io) for the getting
|
||||
started guide, a tutorial, the main API documentation and examples.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "../xlsxwriter.h"
|
||||
#import "app.h"
|
||||
#import "chart.h"
|
||||
#import "chartsheet.h"
|
||||
#import "comment.h"
|
||||
#import "common.h"
|
||||
#import "content_types.h"
|
||||
#import "core.h"
|
||||
#import "custom.h"
|
||||
#import "drawing.h"
|
||||
#import "format.h"
|
||||
#import "hash_table.h"
|
||||
#import "metadata.h"
|
||||
#import "packager.h"
|
||||
#import "relationships.h"
|
||||
#import "shared_strings.h"
|
||||
#import "styles.h"
|
||||
#import "table.h"
|
||||
#import "theme.h"
|
||||
#import "third_party/emyg_dtoa.h"
|
||||
#import "third_party/ioapi.h"
|
||||
#import "third_party/md5.h"
|
||||
#import "third_party/queue.h"
|
||||
#import "third_party/tmpfileplus.h"
|
||||
#import "third_party/tree.h"
|
||||
#import "third_party/zip.h"
|
||||
#import "utility.h"
|
||||
#import "vml.h"
|
||||
#import "workbook.h"
|
||||
#import "worksheet.h"
|
||||
#import "xmlwriter.h"
|
||||
|
||||
FOUNDATION_EXPORT double xlsxwriterVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char xlsxwriterVersionString[];
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
framework module xlsxwriter {
|
||||
umbrella header "xlsxwriter/libxlsxwriter-umbrella.h"
|
||||
header "xlsxwriter.h"
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
This directory contains some release utilities that are mainly useful
|
||||
to the library developer.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#/bin/bash
|
||||
|
||||
# Perform some minor clean-ups/fixes to the docs.
|
||||
|
||||
perl -i -pe "s/_page/_8h/" html/pages.html
|
||||
perl -i ../dev/release/fix_example_docs.pl html/examples.html
|
||||
cp menudata.js html
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
#
|
||||
# Simple program to arrange the example programs in a user defined order
|
||||
# instead of a sorted order. Also add a caption.
|
||||
#
|
||||
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
use warnings;
|
||||
use strict;
|
||||
|
||||
# The required example order and descriptions.
|
||||
my @examples = (
|
||||
[ 'hello.c', 'A simple hello world example' ],
|
||||
[ 'anatomy.c', 'The anatomy of a libxlsxwriter program' ],
|
||||
[ 'demo.c', 'Demo of some of the libxlsxwriter features' ],
|
||||
[ 'tutorial1.c', 'Tutorial 1 from the documentation' ],
|
||||
[ 'tutorial2.c', 'Tutorial 2 from the documentation' ],
|
||||
[ 'tutorial3.c', 'Tutorial 3 from the documentation' ],
|
||||
[ 'format_font.c', 'Example of writing data with font formatting' ],
|
||||
[ 'format_num_format.c', 'Example of writing data with number formatting' ],
|
||||
[ 'dates_and_times01.c', 'Writing dates and times with numbers' ],
|
||||
[ 'dates_and_times02.c', 'Writing dates and times with datetime' ],
|
||||
[ 'dates_and_times03.c', 'Writing dates and times with Unix datetimes' ],
|
||||
[ 'dates_and_times04.c', 'Dates and times with different formats' ],
|
||||
[ 'hyperlinks.c', 'A example of writing urls/hyperlinks' ],
|
||||
[ 'rich_strings.c', 'A example of writing "rich" multi-format strings' ],
|
||||
[ 'array_formula.c', 'A example of using array formulas' ],
|
||||
[ 'dynamic_arrays.c', 'A example of using Excel 365 dynamic array formulas' ],
|
||||
[ 'utf8.c', 'A example of some UTF-8 text' ],
|
||||
[ 'constant_memory.c', 'Write a large file with constant memory usage' ],
|
||||
[ 'output_buffer.c', 'Write a file to a memory buffer' ],
|
||||
[ 'image_buffer.c', 'Example of adding an image from a memory buffer.' ],
|
||||
[ 'merge_range.c', 'Create a merged range of cells' ],
|
||||
[ 'merge_rich_string.c', 'Create a merged range with a rich string' ],
|
||||
[ 'autofilter.c', 'An example of a worksheet autofilter' ],
|
||||
[ 'data_validate.c', 'Examples of worksheet data validation and drop down lists' ],
|
||||
[ 'conditional_format1.c', 'A simple conditional formatting example' ],
|
||||
[ 'conditional_format2.c', 'An advanced conditional formatting example' ],
|
||||
[ 'tables.c', 'Example of table to a worksheet.' ],
|
||||
[ 'images.c', 'Example of adding images to a worksheet.' ],
|
||||
[ 'headers_footers.c', 'Example of adding worksheet headers/footers' ],
|
||||
[ 'defined_name.c', 'Example of how to create defined names' ],
|
||||
[ 'outline.c', 'Example of grouping and outlines' ],
|
||||
[ 'outline_collapsed.c', 'Example of grouping and collapsed outlines' ],
|
||||
[ 'watermark.c', 'Example of how to set a watermark image for a worksheet' ],
|
||||
[ 'background.c', 'Example of how to set the background image for a worksheet' ],
|
||||
[ 'tab_colors.c', 'Example of how to set worksheet tab colors' ],
|
||||
[ 'diagonal_border.c', 'Example of how to set a worksheet cell diagonal border.' ],
|
||||
[ 'hide_sheet.c', 'Example of hiding a worksheet' ],
|
||||
[ 'doc_properties.c', 'Example of setting workbook doc properties' ],
|
||||
[ 'doc_custom_properties.c','Example of setting custom doc properties' ],
|
||||
[ 'worksheet_protection.c', 'Example of enabling worksheet protection' ],
|
||||
[ 'hide_row_col.c', 'Example of hiding worksheet rows and columns' ],
|
||||
[ 'comments1.c', 'Example of adding cell comments to a worksheet' ],
|
||||
[ 'comments2.c', 'Example of adding cell comments with options' ],
|
||||
[ 'macro.c', 'Example of adding a VBA macro to a workbook' ],
|
||||
[ 'panes.c', 'Example of how to create worksheet panes' ],
|
||||
[ 'ignore_errors.c', 'Example of ignoring worksheet errors/warnings' ],
|
||||
[ 'lambda.c', 'Example of using the EXCEL 365+ LAMBDA() function' ],
|
||||
[ 'chart.c', 'Example of a simple column chart' ],
|
||||
[ 'chart_area.c', 'Examples of area charts' ],
|
||||
[ 'chart_bar.c', 'Examples of bar charts' ],
|
||||
[ 'chart_column.c', 'Examples of column charts' ],
|
||||
[ 'chart_line.c', 'Example of a line chart' ],
|
||||
[ 'chart_scatter.c', 'Examples of scatter charts' ],
|
||||
[ 'chart_radar.c', 'Examples of radar charts' ],
|
||||
[ 'chart_pie.c', 'Examples of pie charts' ],
|
||||
[ 'chart_doughnut.c', 'Examples of doughnut charts' ],
|
||||
[ 'chart_clustered.c', 'Examples of clustered category chart' ],
|
||||
[ 'chart_data_table.c', 'Examples of charts with data tables' ],
|
||||
[ 'chart_data_tools.c', 'Examples of charts data tools' ],
|
||||
[ 'chart_data_labels.c', 'Examples of charts data labels' ],
|
||||
[ 'chart_fonts.c', 'Examples of using charts fonts' ],
|
||||
[ 'chart_pattern.c', 'Examples of using charts patterns' ],
|
||||
[ 'chart_styles.c', 'Examples of built-in charts styles' ],
|
||||
[ 'chartsheet.c', 'Example of a chartsheet chart' ],
|
||||
);
|
||||
|
||||
# Convert the array refs to a hash for lookups.
|
||||
my %examples;
|
||||
for my $example (@examples) {
|
||||
$examples{$example->[0]} = 1;
|
||||
}
|
||||
|
||||
my $in_list = 0;
|
||||
|
||||
while ( my $line = <> ) {
|
||||
|
||||
# Print all lines not in the <ul> list.
|
||||
print $line if !$in_list;
|
||||
|
||||
# Check for <ul> list.
|
||||
if ( $line =~ /<div class="textblock">/ ) {
|
||||
$in_list = 1;
|
||||
}
|
||||
|
||||
# Capture the <li> items of the list.
|
||||
if ( $line =~ /<li><a class="el" href="[^"]+">([^<]+)/ ) {
|
||||
my $example = $1;
|
||||
|
||||
# Warn if there are any new/unkown items.
|
||||
if ( !exists $examples{$example} ) {
|
||||
warn "$0 Unknown example: $example\n";
|
||||
}
|
||||
next;
|
||||
}
|
||||
|
||||
# At the end of the <ul> list print out the <li> items in user defined order.
|
||||
if ( $line =~ m{^</ul>} ) {
|
||||
$in_list = 0;
|
||||
|
||||
for my $aref ( @examples ) {
|
||||
my $example = $aref->[0];
|
||||
my $filename = $aref->[0];
|
||||
my $desc = $aref->[1];
|
||||
|
||||
$example =~ s/\.c/_8c-example.html/;
|
||||
|
||||
printf qq(<li><a class="el" href="%s">%s</a> %s</li>\n\n),
|
||||
$example, $filename, $desc;
|
||||
}
|
||||
print $line;
|
||||
}
|
||||
}
|
||||
|
||||
__END__
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
#
|
||||
# Simple program to generate the examples.dox file from a simple text file,
|
||||
# with links to the next/previous examples.
|
||||
#
|
||||
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
my @examples;
|
||||
my @sections;
|
||||
my @links;
|
||||
my $buffer = '';
|
||||
|
||||
|
||||
# Sample through the example sections and break the text into blocks.
|
||||
while ( my $line = <> ) {
|
||||
|
||||
# Ignore comments in the input file.
|
||||
next if $line =~ /^#/;
|
||||
|
||||
# Match the start of an example block.
|
||||
if ( $line =~ /^\@example/ ) {
|
||||
chomp $buffer;
|
||||
|
||||
# Store the example name and the section body.
|
||||
push @examples, $line;
|
||||
push @sections, $buffer;
|
||||
$buffer = '';
|
||||
next;
|
||||
}
|
||||
|
||||
$buffer .= $line;
|
||||
}
|
||||
|
||||
# Store the last example section and omit the first blank element.
|
||||
push @sections, $buffer;
|
||||
shift @sections;
|
||||
|
||||
# Generate a set of @ref links targets from the example program names.
|
||||
for ( @examples ) {
|
||||
my $link = $_;
|
||||
chomp $link;
|
||||
$link =~ s/\@example //;
|
||||
push @links, [ $link, $link ];
|
||||
}
|
||||
|
||||
# Add the first and last links back to the examples.
|
||||
unshift @links, [ "examples", "Examples page" ];
|
||||
push @links, [ "examples", "Examples page" ];
|
||||
|
||||
# Add the start of the Doxygen header.
|
||||
print "/**\n";
|
||||
print "\@page examples Example Programs\n\n";
|
||||
|
||||
# Print out each section.
|
||||
for my $i ( 0 .. @examples - 1 ) {
|
||||
|
||||
print $examples[$i];
|
||||
|
||||
# Add a simple header table with next/previous links.
|
||||
printf qq{\n<table width="600">\n};
|
||||
printf qq{<tr>\n};
|
||||
printf qq{ <td>\@ref %s "<< %s"</td>\n},
|
||||
$links[$i]->[0], $links[$i]->[1];
|
||||
printf qq{ <td align="right">\@ref %s "%s >>"</td>\n},
|
||||
$links[ $i + 2 ]->[0], $links[ $i + 2 ]->[1];
|
||||
printf qq{</tr>\n};
|
||||
printf qq{</table>\n};
|
||||
|
||||
print $sections[$i], "\n\n\n\n";
|
||||
}
|
||||
|
||||
# Print the end of the doxygen comment.
|
||||
print "*/\n";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
#
|
||||
# Simple program to generate the string array for the lxw_strerror() function
|
||||
# from the Doxygen comments in the lxw_error enum:
|
||||
#
|
||||
# perl dev/release/gen_error_strings.pl include/xlsxwriter/common.h
|
||||
#
|
||||
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
use warnings;
|
||||
use strict;
|
||||
|
||||
my $in_enum = 0;
|
||||
my @strings;
|
||||
|
||||
my $filename = shift || 'include/xlsxwriter/common.h';
|
||||
open my $fh, '<', $filename or die "Couldn't open $filename: $!\n";
|
||||
|
||||
|
||||
while (<$fh>) {
|
||||
|
||||
$in_enum = 1 if /typedef enum lxw_error/;
|
||||
$in_enum = 0 if /} lxw_error;/;
|
||||
|
||||
# Match doxygen strings in the enum.
|
||||
if ($in_enum && m{/\*\*}) {
|
||||
# Strip the comment parts.
|
||||
s{/\*\*}{};
|
||||
s{\*/}{};
|
||||
s{^\s+}{};
|
||||
s{\s+$}{};
|
||||
push @strings, $_;
|
||||
}
|
||||
}
|
||||
|
||||
# Print out an array of strings based on the doxygen comments.
|
||||
print "\n";
|
||||
print "// Copy to src/utility.c\n\n";
|
||||
print "char *error_strings[LXW_MAX_ERRNO + 1] = {\n";
|
||||
for my $string (@strings) {
|
||||
print qq{ "$string",\n};
|
||||
}
|
||||
print qq{ "Unknown error number."\n};
|
||||
print "};\n\n";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
#
|
||||
# Simple program to generate the coccoapods unbrella file.
|
||||
# Run from the libxlsxwriter root dir.
|
||||
#
|
||||
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
use warnings;
|
||||
use strict;
|
||||
use File::Find;
|
||||
|
||||
my @includes;
|
||||
|
||||
# Callback to match header files.
|
||||
sub match_include {
|
||||
push @includes, $File::Find::name if /^.*\.h\z/s;
|
||||
}
|
||||
|
||||
# Use File::Find to find header files.
|
||||
find({wanted => \&match_include}, 'include/xlsxwriter');
|
||||
|
||||
# Sort and remove leading dirs from the include files.
|
||||
@includes = sort @includes;
|
||||
s{^include/xlsxwriter/}{} for @includes;
|
||||
|
||||
|
||||
# Generate the unbrella file.
|
||||
print qq{#import <Foundation/Foundation.h>\n\n};
|
||||
print qq{#import "../xlsxwriter.h"\n};
|
||||
|
||||
print qq{#import "$_"\n} for @includes;
|
||||
|
||||
print qq{\n};
|
||||
print qq{FOUNDATION_EXPORT double xlsxwriterVersionNumber;\n};
|
||||
print qq{FOUNDATION_EXPORT const unsigned char xlsxwriterVersionString[];\n\n};
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
#
|
||||
# Simple program to generate a Windows .def file from the exported symbols in
|
||||
# libxlsxwriter.a.
|
||||
#
|
||||
# perl dev/release/gen_windows_def_file.pl lib/libxlsxwriter.a
|
||||
#
|
||||
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
use warnings;
|
||||
use strict;
|
||||
|
||||
my $lib_file = shift;
|
||||
|
||||
die "$0: Path to .a lib file required.\n" if !$lib_file;
|
||||
die "$0: File '$lib_file' not found\n" if !-e $lib_file;
|
||||
|
||||
# Get the symbols from the libxlsxwriter.a file.
|
||||
my @symbols = `nm $lib_file`;
|
||||
my %unique;
|
||||
|
||||
for my $symbol ( @symbols ) {
|
||||
|
||||
chomp $symbol;
|
||||
|
||||
# Get the last field in the row.
|
||||
my @fields = split " ", $symbol;
|
||||
$symbol = $fields[-1];
|
||||
|
||||
next unless $symbol;
|
||||
|
||||
# Skip symbols not belonging to libxlsxwriter.
|
||||
next if $symbol !~ /^_(lxw|work|format|chart|new)/;
|
||||
|
||||
# Skip some the RedBlack functions.
|
||||
next if $symbol =~ m{RB};
|
||||
|
||||
# Strip the leading underscore.
|
||||
$symbol =~ s/^_//;
|
||||
|
||||
# Remove duplicate instances of some symbols.
|
||||
$unique{$symbol}++;
|
||||
}
|
||||
|
||||
# Generate the .def file.
|
||||
print "EXPORTS\r\n";
|
||||
for my $symbol ( sort keys %unique ) {
|
||||
print " ", $symbol, "\r\n";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
prefix=@PREFIX@
|
||||
exec_prefix=${prefix}
|
||||
includedir=${prefix}/include
|
||||
libdir=${exec_prefix}/lib
|
||||
|
||||
Name: libxlsxwriter
|
||||
Description: A C library for creating Excel XLSX files
|
||||
Version: @VERSION@
|
||||
Cflags: -I${includedir}
|
||||
Libs: -L${libdir} -lxlsxwriter -lz
|
||||
@@ -0,0 +1,332 @@
|
||||
#!/bin/bash
|
||||
|
||||
clear
|
||||
echo "|"
|
||||
echo "| Pre-release checks."
|
||||
echo "|"
|
||||
echo
|
||||
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# Run tests.
|
||||
#
|
||||
function check_test_status {
|
||||
|
||||
echo
|
||||
echo -n "Are all tests passing? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
|
||||
echo -n " Run all tests now? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo
|
||||
echo -e "Please run: make test\n";
|
||||
exit 1
|
||||
else
|
||||
echo " Running tests...";
|
||||
make test
|
||||
check_test_status
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# Run test for C++ const correctness.
|
||||
#
|
||||
function check_test_const {
|
||||
|
||||
echo
|
||||
echo -n "Is the const test passing? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
|
||||
echo -n " Run test now? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo
|
||||
echo -e "Please run: make test_const\n";
|
||||
exit 1
|
||||
else
|
||||
echo " Running test...";
|
||||
make test_const
|
||||
check_test_const
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# Run spellcheck.
|
||||
#
|
||||
function check_spellcheck {
|
||||
|
||||
echo
|
||||
echo -n "Is the spellcheck ok? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
|
||||
echo -n " Run spellcheck now? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo
|
||||
echo -e "Please run: make spellcheck\n";
|
||||
exit 1
|
||||
else
|
||||
echo " Running spellcheck...";
|
||||
make spellcheck
|
||||
check_spellcheck
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# Check Changes file is up to date.
|
||||
#
|
||||
function check_changefile {
|
||||
clear
|
||||
|
||||
echo "Latest change in Changes file: "
|
||||
perl -ne '$rev++ if /^##/; exit if $rev > 1; print " | $_"' Changes.txt
|
||||
|
||||
echo
|
||||
echo -n "Is the Changes file updated? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo
|
||||
echo -e "Please update the Change file to proceed.\n";
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# Check the versions are up to date.
|
||||
#
|
||||
function check_versions {
|
||||
|
||||
clear
|
||||
echo
|
||||
echo "Latest file versions: "
|
||||
echo
|
||||
|
||||
awk '/s.version / {print "\t" FILENAME "\t" $1 "\t" $3}' libxlsxwriter.podspec
|
||||
awk '/ LXW/ {print "\t" FILENAME "\t" $2 "\t" $3}' include/xlsxwriter.h
|
||||
|
||||
echo
|
||||
echo -n "Are the versions up to date? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo -n " Update versions? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo
|
||||
echo -e "Please update the versions to proceed.\n";
|
||||
exit 1
|
||||
else
|
||||
echo " Updating versions...";
|
||||
perl -i dev/release/update_revison.pl include/xlsxwriter.h libxlsxwriter.podspec
|
||||
check_versions
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# Check that the docs build cleanly.
|
||||
#
|
||||
function check_docs {
|
||||
|
||||
# clear
|
||||
echo
|
||||
echo -n "Do the docs build cleanly? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
|
||||
echo -n " Build docs now? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo
|
||||
echo -e "Please run: make docs\n";
|
||||
exit 1
|
||||
else
|
||||
echo " Building docs...";
|
||||
make docs
|
||||
check_docs
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# Generate the cocoapods umbrella file.
|
||||
#
|
||||
function gen_umbrella_file {
|
||||
|
||||
echo
|
||||
echo -n "Is the umbrella file up to date? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
|
||||
echo -n " Update umbrella file now? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo
|
||||
echo -e "Please update cocoapods/libxlsxwriter-umbrella.h\n";
|
||||
exit 1
|
||||
else
|
||||
echo " Updating file...";
|
||||
perl dev/release/gen_umbrella_file.pl > cocoapods/libxlsxwriter-umbrella.h
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# Check the cocoapods spec file.
|
||||
#
|
||||
function check_pod_spec {
|
||||
|
||||
echo
|
||||
echo -n "Is the coacoapod file ok? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
|
||||
echo -n " Run lint now? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo
|
||||
echo -e "Please run: pod spec lint libxlsxwriter.podspec\n";
|
||||
exit 1
|
||||
else
|
||||
echo " Running lint...";
|
||||
pod spec lint libxlsxwriter.podspec --use-libraries
|
||||
check_pod_spec
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# Update the pod repo. This can take some time.
|
||||
#
|
||||
function update_pod_repo {
|
||||
|
||||
echo
|
||||
echo -n "Is the pod repo updated? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
|
||||
echo -n " Update now? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo
|
||||
echo -e "Please run: pod spec lint libxlsxwriter.podspec\n";
|
||||
exit 1
|
||||
else
|
||||
echo " Running update...";
|
||||
cd ~/.cocoapods/repos/master
|
||||
git pull --ff-only
|
||||
cd -
|
||||
update_pod_repo
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# Run release checks.
|
||||
#
|
||||
function check_git_status {
|
||||
|
||||
clear
|
||||
|
||||
echo "Git status: "
|
||||
git status | awk '{print " | ", $0}'
|
||||
|
||||
echo "Git log: "
|
||||
git log -1 | awk '{print " | ", $0}'
|
||||
|
||||
echo "Git latest tag: "
|
||||
git tag -l -n1 | tail -1 | awk '{print " | ", $0}'
|
||||
|
||||
echo
|
||||
echo -n "Is the git status okay? [y/N]: "
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" != "y" ]; then
|
||||
echo
|
||||
echo -e "Please fix git status.\n";
|
||||
|
||||
echo -e "\ngit add -u";
|
||||
git tag -l -n1 | tail -1 | perl -lane 'printf "git commit -m \"Prep for release %s\"\ngit tag \"%s\"\n\n", $F[4], $F[0]' | perl dev/release/update_revison.pl
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_test_status
|
||||
clear
|
||||
check_test_const
|
||||
clear
|
||||
check_spellcheck
|
||||
clear
|
||||
check_docs
|
||||
check_changefile
|
||||
clear
|
||||
gen_umbrella_file
|
||||
check_pod_spec
|
||||
clear
|
||||
update_pod_repo
|
||||
check_versions
|
||||
check_git_status
|
||||
|
||||
|
||||
#############################################################
|
||||
#
|
||||
# All checks complete.
|
||||
#
|
||||
clear
|
||||
echo
|
||||
echo "Everything is configured.";
|
||||
echo
|
||||
|
||||
echo -n "Confirm release: [y/N]: ";
|
||||
read RESPONSE
|
||||
|
||||
if [ "$RESPONSE" == "y" ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
# Simple script to increment x.y.z style version numbers in a file.
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Perl::Version;
|
||||
|
||||
while (<>) {
|
||||
|
||||
# Increment any x.y.z version strings.
|
||||
if (m/(\d\.\d\.\d)/) {
|
||||
my $version = Perl::Version->new( $1 );
|
||||
|
||||
# Components are: revision, version and subversion.
|
||||
if ( $version->version == 9 && $version->subversion == 9 ) {
|
||||
$version->inc_revision();
|
||||
}
|
||||
elsif ( $version->subversion == 9 ) {
|
||||
$version->inc_version();
|
||||
}
|
||||
else {
|
||||
$version->inc_subversion();
|
||||
}
|
||||
|
||||
my $new_version = $version->stringify();
|
||||
s/\d\.\d\.\d/$new_version/;
|
||||
}
|
||||
|
||||
# Increment the LXW_VERSION_ID number in xlsxwriter.h
|
||||
if (m/LXW_VERSION_ID (\d+)/) {
|
||||
my $version = $1;
|
||||
my $new_version = $version + 1;
|
||||
|
||||
s/\d+/$new_version/;
|
||||
}
|
||||
|
||||
print;
|
||||
}
|
||||
|
||||
|
||||
__END__
|
||||
@@ -0,0 +1,180 @@
|
||||
<doxygenlayout version="1.0">
|
||||
<!-- Generated by doxygen 1.8.7 -->
|
||||
<!-- Navigation index tabs for HTML output -->
|
||||
<navindex>
|
||||
<tab type="mainpage" visible="yes" title=""/>
|
||||
<tab type="modules" visible="yes" title="" intro=""/>
|
||||
<tab type="pages" visible="yes" title="" intro=""/>
|
||||
<tab type="files" visible="yes" title="">
|
||||
<tab type="filelist" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="examples" visible="yes" title="" intro="Example programs using libxlsxwriter:"/>
|
||||
</navindex>
|
||||
|
||||
<!-- Layout definition for a class page -->
|
||||
<class>
|
||||
<briefdescription visible="yes"/>
|
||||
<detaileddescription title=""/>
|
||||
<inheritancegraph visible="$CLASS_GRAPH"/>
|
||||
<collaborationgraph visible="$COLLABORATION_GRAPH"/>
|
||||
<memberdecl>
|
||||
<nestedclasses visible="yes" title=""/>
|
||||
<publictypes title=""/>
|
||||
<services title=""/>
|
||||
<interfaces title=""/>
|
||||
<publicslots title=""/>
|
||||
<signals title=""/>
|
||||
<publicmethods title=""/>
|
||||
<publicstaticmethods title=""/>
|
||||
<publicattributes title=""/>
|
||||
<publicstaticattributes title=""/>
|
||||
<protectedtypes title=""/>
|
||||
<protectedslots title=""/>
|
||||
<protectedmethods title=""/>
|
||||
<protectedstaticmethods title=""/>
|
||||
<protectedattributes title=""/>
|
||||
<protectedstaticattributes title=""/>
|
||||
<packagetypes title=""/>
|
||||
<packagemethods title=""/>
|
||||
<packagestaticmethods title=""/>
|
||||
<packageattributes title=""/>
|
||||
<packagestaticattributes title=""/>
|
||||
<properties title=""/>
|
||||
<events title=""/>
|
||||
<privatetypes title=""/>
|
||||
<privateslots title=""/>
|
||||
<privatemethods title=""/>
|
||||
<privatestaticmethods title=""/>
|
||||
<privateattributes title=""/>
|
||||
<privatestaticattributes title=""/>
|
||||
<friends title=""/>
|
||||
<related title="" subtitle=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<memberdef>
|
||||
<inlineclasses title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<services title=""/>
|
||||
<interfaces title=""/>
|
||||
<constructors title=""/>
|
||||
<functions title=""/>
|
||||
<related title=""/>
|
||||
<variables title=""/>
|
||||
<properties title=""/>
|
||||
<events title=""/>
|
||||
</memberdef>
|
||||
<allmemberslink visible="yes"/>
|
||||
<usedfiles visible="$SHOW_USED_FILES"/>
|
||||
<authorsection visible="yes"/>
|
||||
</class>
|
||||
|
||||
<!-- Layout definition for a namespace page -->
|
||||
<namespace>
|
||||
<briefdescription visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestednamespaces visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</namespace>
|
||||
|
||||
<!-- Layout definition for a file page -->
|
||||
<file>
|
||||
<detaileddescription title="Description"/>
|
||||
<sourcelink visible="yes"/>
|
||||
<memberdecl>
|
||||
<functions title=""/>
|
||||
</memberdecl>
|
||||
<memberdef>
|
||||
<functions title=""/>
|
||||
<inlineclasses title=""/>
|
||||
<typedefs title=""/>
|
||||
<defines title=""/>
|
||||
<enums title=""/>
|
||||
<variables title=""/>
|
||||
</memberdef>
|
||||
<memberdecl>
|
||||
<classes visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<typedefs title=""/>
|
||||
<variables title=""/>
|
||||
<enums title=""/>
|
||||
<defines title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<authorsection/>
|
||||
</file>
|
||||
|
||||
<!-- Layout definition for a group page -->
|
||||
<group>
|
||||
<briefdescription visible="yes"/>
|
||||
<groupgraph visible="$GROUP_GRAPHS"/>
|
||||
<memberdecl>
|
||||
<nestedgroups visible="yes" title=""/>
|
||||
<dirs visible="yes" title=""/>
|
||||
<files visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<enumvalues title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<signals title=""/>
|
||||
<publicslots title=""/>
|
||||
<protectedslots title=""/>
|
||||
<privateslots title=""/>
|
||||
<events title=""/>
|
||||
<properties title=""/>
|
||||
<friends title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<pagedocs/>
|
||||
<inlineclasses title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<enumvalues title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<signals title=""/>
|
||||
<publicslots title=""/>
|
||||
<protectedslots title=""/>
|
||||
<privateslots title=""/>
|
||||
<events title=""/>
|
||||
<properties title=""/>
|
||||
<friends title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</group>
|
||||
|
||||
<!-- Layout definition for a directory page -->
|
||||
<directory>
|
||||
<briefdescription visible="yes"/>
|
||||
<directorygraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<dirs visible="yes"/>
|
||||
<files visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
</directory>
|
||||
</doxygenlayout>
|
||||
@@ -0,0 +1,28 @@
|
||||
###############################################################################
|
||||
#
|
||||
# Makefile for libxlsxwriter library.
|
||||
#
|
||||
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
|
||||
#
|
||||
|
||||
# Keep the output quiet by default.
|
||||
Q=@
|
||||
ifdef V
|
||||
Q=
|
||||
endif
|
||||
|
||||
# Make everything.
|
||||
all : docs
|
||||
|
||||
# Clean up.
|
||||
clean :
|
||||
$(Q)rm -rf html/*
|
||||
|
||||
# Build the doxygen docs.
|
||||
docs:
|
||||
$(Q)perl ../dev/release/fix_example_links.pl src/examples.txt > src/examples.dox
|
||||
$(Q)doxygen
|
||||
$(Q)../dev/release/fix_dox.sh
|
||||
|
||||
docs_doxygen_only:
|
||||
$(Q)doxygen
|
||||
@@ -0,0 +1,18 @@
|
||||
<!-- HTML footer for doxygen 1.8.20-->
|
||||
<!-- start footer part -->
|
||||
<!--BEGIN GENERATE_TREEVIEW-->
|
||||
<div id="nav-path" class="navpath"><!-- id is needed for treeview function! -->
|
||||
<ul>
|
||||
$navpath
|
||||
<li class="footer">Copyright 2014-2022 John McNamara. $generatedby <a href="http://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion </li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--END GENERATE_TREEVIEW-->
|
||||
<!--BEGIN !GENERATE_TREEVIEW-->
|
||||
<hr class="footer"/><address class="footer"><small>
|
||||
Copyright 2014-2022 John McNamara.
|
||||
$generatedby <a href="http://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion
|
||||
</small></address>
|
||||
<!--END !GENERATE_TREEVIEW-->
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 61 KiB |